diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5476f1d9e02fe2ab51d972a6d9f0c1385276e50b..f8f5b80ad912300f4bd05861a54f8f1cd483b8c1 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -63,7 +63,7 @@ gradle_build:
   stage: build
   rules:
     - if: $CI_COMMIT_BRANCH == "master" ||
-        $CI_COMMIT_BRANCH == "dev" ||
+        $CI_COMMIT_BRANCH == "development" ||
         $CI_MERGE_REQUEST_ID ||
         $CI_PIPELINE_SOURCE == "push" ||
         $CI_PIPELINE_SOURCE == "trigger"
@@ -91,7 +91,7 @@ gradle_test:
   rules:
     - if: $CI_PIPELINE_SOURCE == "trigger"
       when: never
-    - if: $CI_COMMIT_BRANCH == "dev" ||
+    - if: $CI_COMMIT_BRANCH == "development" ||
         $CI_MERGE_REQUEST_ID ||
         $CI_PIPELINE_SOURCE == "push"
   coverage: '/Code coverage: \d+\.\d+/'
@@ -128,7 +128,7 @@ mysql_migration:
     - if: $CI_PIPELINE_SOURCE == "trigger"
       when: never
     - if: $CI_COMMIT_BRANCH == "master" ||
-        $CI_COMMIT_BRANCH == "dev" ||
+        $CI_COMMIT_BRANCH == "development" ||
         $CI_MERGE_REQUEST_ID ||
         $CI_PIPELINE_SOURCE == "push"
   stage: prepare
@@ -170,7 +170,7 @@ postgreSQL_migration:
     - if: $CI_PIPELINE_SOURCE == "trigger"
       when: never
     - if: $CI_COMMIT_BRANCH == "master" ||
-        $CI_COMMIT_BRANCH == "dev" ||
+        $CI_COMMIT_BRANCH == "development" ||
         $CI_MERGE_REQUEST_ID ||
         $CI_PIPELINE_SOURCE == "push"
   stage: prepare
@@ -215,7 +215,7 @@ mysql_test:
 
     # Run on development and master branches.
     - if: $CI_COMMIT_BRANCH == "master" ||
-        $CI_COMMIT_BRANCH == "dev"
+        $CI_COMMIT_BRANCH == "development"
 
     # Only run on merge requests that change one of the model/migration files otherwise.
     - if: $CI_PIPELINE_SOURCE != "merge_request_event"
@@ -256,7 +256,7 @@ mysql_test:
 #
 #    # Always run on development and master branches.
 #    - if: $CI_COMMIT_BRANCH == "master" ||
-#        $CI_COMMIT_BRANCH == "dev"
+#        $CI_COMMIT_BRANCH == "development"
 #
 #    # Only run on merge requests that change one of the model/migration files otherwise.
 #    - if: $CI_PIPELINE_SOURCE != "merge_request_event"
@@ -301,7 +301,7 @@ gradle_spotless:
     - if: $CI_PIPELINE_SOURCE == "trigger"
       when: never
     - if: $CI_COMMIT_BRANCH == "master" ||
-        $CI_COMMIT_BRANCH == "dev" ||
+        $CI_COMMIT_BRANCH == "development" ||
         $CI_MERGE_REQUEST_ID ||
         $CI_PIPELINE_SOURCE == "push"
   artifacts:
@@ -327,7 +327,7 @@ gradle_licenses:
     - if: $CI_PIPELINE_SOURCE == "trigger"
       when: never
     - if: $CI_COMMIT_BRANCH == "master" ||
-        $CI_COMMIT_BRANCH == "dev" ||
+        $CI_COMMIT_BRANCH == "development" ||
         $CI_MERGE_REQUEST_ID ||
         $CI_PIPELINE_SOURCE == "push"
   stage: review
@@ -346,7 +346,7 @@ publish_jar:
   stage: publish
   rules:
     - if: $CI_COMMIT_BRANCH == "master" ||
-        $CI_COMMIT_BRANCH == "dev" ||
+        $CI_COMMIT_BRANCH == "development" ||
         $CI_MERGE_REQUEST_ID ||
         $CI_PIPELINE_SOURCE == "push" ||
         $CI_PIPELINE_SOURCE == "trigger"
@@ -448,7 +448,7 @@ code_quality:
         $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train"
       when: never
     - if: $CI_COMMIT_BRANCH == "master" ||
-        $CI_COMMIT_BRANCH == "dev" ||
+        $CI_COMMIT_BRANCH == "development" ||
         $CI_MERGE_REQUEST_ID
   stage: report
 
@@ -462,7 +462,7 @@ spotbugs-sast:
         $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train"
       when: never
     - if: $CI_COMMIT_BRANCH == "master" ||
-        $CI_COMMIT_BRANCH == "dev" ||
+        $CI_COMMIT_BRANCH == "development" ||
         $CI_MERGE_REQUEST_ID
   stage: report
   needs:
@@ -480,7 +480,7 @@ dast:
     - if: $CI_PIPELINE_SOURCE == "trigger" ||
         $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train"
       when: never
-    - if: $CI_COMMIT_BRANCH == "dev"
+    - if: $CI_COMMIT_BRANCH == "development"
       when: manual
   stage: live report
   variables:
@@ -499,7 +499,7 @@ secret_detection:
         $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train"
       when: never
     - if: $CI_COMMIT_BRANCH == "master" ||
-        $CI_COMMIT_BRANCH == "dev" ||
+        $CI_COMMIT_BRANCH == "development" ||
         $CI_MERGE_REQUEST_ID
   stage: report
 
@@ -513,7 +513,7 @@ gemnasium-maven-dependency_scanning:
         $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train"
       when: never
     - if: $CI_COMMIT_BRANCH == "master" ||
-        $CI_COMMIT_BRANCH == "dev" ||
+        $CI_COMMIT_BRANCH == "development" ||
         $CI_MERGE_REQUEST_ID
   needs:
     - gradle_build
@@ -526,7 +526,7 @@ a11y:
     - if: $CI_PIPELINE_SOURCE == "trigger" ||
         $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train"
       when: never
-    - if: $CI_COMMIT_BRANCH == "dev"
+    - if: $CI_COMMIT_BRANCH == "development"
   stage: live report
   variables:
     ally_urls: "https://submit.eiptest.ewi.tudelft.nl"
@@ -542,7 +542,7 @@ browser_performance:
     - if: $CI_PIPELINE_SOURCE == "trigger" ||
         $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train"
       when: never
-    - if: $CI_COMMIT_BRANCH == "dev"
+    - if: $CI_COMMIT_BRANCH == "development"
   stage: live report
   variables:
     URL: "https://submit.eiptest.ewi.tudelft.nl"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe87914e690eb086eca1b13299ae68b3ea10ce1b..67103445f6d0c7ef294c4316d19022bc09e661cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+
+## [2.2.1]
+
+### Added
+- Ability to import groups from csv file (@dsavvidi)
+- Markdown support for script feedback (@rwbackx)
+
+### Changed
+
+### Fixed
+- Fixed bug where assistant view resets selected module upon search (@dsavvidi)
+- Fixed bug where module grades didn't work when name started with a number (@dsavvidi)
+
+## [2.2.0]
 ### Added
 - Added delete grade button in group page submissions (@dsavvidi)
 - Ability to change group size for all groups (@dsavvidi)
@@ -20,7 +34,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Added markdown support when giving feedback (@dsavvidi)
 - Module level grade export features (@dsavvidi)
 - Can now view the submission content in browser (@mmadara)
+- Ability to show/hide assignments (@dsavvidi)
 - New statistic: Average number of submissions before passing per assignment (@dsavvidi)
+- Ability to make test submissions for teachers and head TAs (@dsavvidi)
 
 ### Changed
 - Group joining is now restricted to groups with no submissions (@dsavvidi)
@@ -30,7 +46,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Changed the 'download selected submissions' button into 'download all' and 'download latest' (@mmadara)
 
 ### Fixed
+- The dropdown for groups in the Assistant view doesn't reload the page anymore(@mmadara)
 - Modules and assignments were not sorted (@dsavvidi)
+- Students could be a part of multiple groups in a module (@aturgut)
 - Fixed bug where changing the staff role did not work (@dsavvidi)
 - Email link to settings was hardcoded (@dsavvidi)
 
diff --git a/build.gradle.kts b/build.gradle.kts
index edf0679cf8cd85731d4fec58a1795ee613610850..262a0c9141208940cc9b73986c831b41320a0bcc 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,9 +2,10 @@ import com.diffplug.gradle.spotless.SpotlessExtension
 import org.springframework.boot.gradle.tasks.bundling.BootJar
 import nl.javadude.gradle.plugins.license.DownloadLicensesExtension
 import nl.javadude.gradle.plugins.license.LicenseExtension
+import java.nio.file.Files
 
 group = "nl.tudelft.submit"
-version = "2.2.0"
+version = "2.2.1"
 
 val javaVersion = JavaVersion.VERSION_17
 
@@ -71,8 +72,6 @@ plugins {
     // Open API generator for generating the Client code for Labracore.
 //    is this needed?
 //    id("org.openapi.generator").version("4.2.3")
-
-	id("com.unclezs.gradle.sass").version("1.0.10")
 }
 
 sourceSets {
@@ -173,8 +172,6 @@ val jacocoTestReport by tasks.getting(JacocoReport::class) {
 // Task for generating the client code for connecting with the Labracore API
 // val generateSubmitClient?
 
-val processResources by tasks.getting(ProcessResources::class)
-
 // Configure Spring Boot plugin task for running the application.
 val bootJar by tasks.getting(BootJar::class) {
     enabled = true
@@ -218,9 +215,40 @@ tasks.withType<Test>().configureEach {
 }
 
 // Configure the sass compiling task to use the sass and css directories.
-sass {
-	cssPath = "static/css"
-	sassPath = "scss"
+tasks.register("ensureDirectory") {
+    // Store target directory into a variable to avoid project reference in the configuration cache
+    val directories = listOf(file("src/main/resources/static/css"), file("src/main/resources/scss"))
+
+    doLast {
+        directories.forEach { directory ->
+            Files.createDirectories(directory.toPath())
+        }
+    }
+}
+task<Exec>("sassCompile") {
+    dependsOn.add(tasks.getByName("ensureDirectory"))
+    if (System.getProperty("os.name").contains("windows",true)) {
+        commandLine("cmd", "/c", "sass", "src/main/resources/scss:src/main/resources/static/css")
+    } else {
+        commandLine("echo", "Checking for sass or sassc...")
+        doLast {
+            val res = exec {
+                isIgnoreExitValue = true
+                executable = "bash"
+                args = listOf("-l", "-c", "sass --version")
+            }
+            if (res.exitValue == 0) {
+                exec { commandLine("sass", "src/main/resources/scss:src/main/resources/static/css") }
+            } else {
+                File("src/main/resources/scss").listFiles()!!.filter { it.extension == "scss" && !it.name.startsWith("_") }.forEach {
+                    exec { commandLine("sassc", "src/main/resources/scss/${it.name}", "src/main/resources/static/css/${it.nameWithoutExtension}.css") }
+                }
+            }
+        }
+    }
+}
+val processResources by tasks.getting(ProcessResources::class) {
+    dependsOn.add(tasks.getByName("sassCompile"))
 }
 
 dependencies {
@@ -236,6 +264,9 @@ dependencies {
     // Guava
     implementation("com.google.guava:guava:11.0.2")
 
+    // Apache Commons
+    implementation("org.apache.commons:commons-csv:1.10.0")
+
     implementation("com.github.ulisesbocchio:spring-boot-security-saml:$samlVersion")
 
     // Hibernate for database-entity mapping and EntityManagers
diff --git a/src/main/java/nl/tudelft/submit/controller/AssignmentController.java b/src/main/java/nl/tudelft/submit/controller/AssignmentController.java
index efd210c21be6bf2e56e82ab158e44fbbd52a5389..9e8d86c4219624619b395588fe98ed7c2d60e36e 100644
--- a/src/main/java/nl/tudelft/submit/controller/AssignmentController.java
+++ b/src/main/java/nl/tudelft/submit/controller/AssignmentController.java
@@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
@@ -51,6 +52,7 @@ import com.opencsv.exceptions.CsvValidationException;
 import nl.tudelft.labracore.api.dto.*;
 import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
 import nl.tudelft.labracore.lib.security.user.Person;
+import nl.tudelft.librador.dto.view.View;
 import nl.tudelft.submit.cache.PersonCacheManager;
 import nl.tudelft.submit.cache.StudentGroupCacheManager;
 import nl.tudelft.submit.dto.create.SubmitAssignmentCreateDTO;
@@ -70,8 +72,10 @@ import nl.tudelft.submit.dto.view.FeedbackViewDTO;
 import nl.tudelft.submit.dto.view.VersionViewDTO;
 import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentDetailsDTO;
 import nl.tudelft.submit.dto.view.labracore.SubmitSubmissionViewDTO;
+import nl.tudelft.submit.dto.view.script.ScriptTrainResultViewDTO;
 import nl.tudelft.submit.external.PageUtil;
 import nl.tudelft.submit.model.Signature;
+import nl.tudelft.submit.model.TestSubmission;
 import nl.tudelft.submit.security.AuthorizationService;
 import nl.tudelft.submit.service.*;
 
@@ -166,6 +170,15 @@ public class AssignmentController {
 			model.addAttribute("version", version);
 			model.addAttribute("availableGroups", availableGroups);
 
+			Optional<TestSubmission> sub = submissionService
+					.getLatestTestSubmissionByAssignment(assignment.getId());
+			if (sub.isPresent()) {
+				List<ScriptTrainResultViewDTO> scriptResults = View.convert(sub.get().getScriptResults(),
+						ScriptTrainResultViewDTO.class);
+				model.addAttribute("testSubmission", sub.get());
+				model.addAttribute("testScriptResults", scriptResults);
+			}
+
 			if (isTransparent) {
 				model.addAttribute("train",
 						scriptService.getScriptTrainById(versions.get(0).getScriptTrain().getId()));
@@ -360,4 +373,17 @@ public class AssignmentController {
 		return "redirect:/assignment/{id}";
 	}
 
+	/**
+	 * Toggles the visibility of an assignment.
+	 *
+	 * @param  id The id of the assignment.
+	 * @return    The page to load
+	 */
+	@PostMapping("{id}/change-visibility")
+	@PreAuthorize("@authorizationService.canEditAssignment(#id)")
+	public String toggleHide(@PathVariable Long id) {
+		assignmentService.toggleHide(id);
+		return "redirect:/assignment/" + id;
+	}
+
 }
diff --git a/src/main/java/nl/tudelft/submit/controller/GradeController.java b/src/main/java/nl/tudelft/submit/controller/GradeController.java
index d1da7681e4c7890d13796b28f23a4e90e189d17a..dfcfbcd3fb19961a446e367541fbd782611e1054 100644
--- a/src/main/java/nl/tudelft/submit/controller/GradeController.java
+++ b/src/main/java/nl/tudelft/submit/controller/GradeController.java
@@ -42,6 +42,7 @@ import nl.tudelft.submit.model.grading.CalculatedScore;
 import nl.tudelft.submit.model.grading.GradingFormula;
 import nl.tudelft.submit.service.AssignmentService;
 import nl.tudelft.submit.service.GradeService;
+import nl.tudelft.submit.service.SubmissionService;
 
 @Transactional
 @RequestMapping("/grade")
@@ -52,6 +53,8 @@ public class GradeController {
 	private GradeService gradeService;
 	@Autowired
 	private AssignmentService assignmentService;
+	@Autowired
+	private SubmissionService submissionService;
 
 	@PostMapping("/group/{groupId}/{assignmentId}")
 	@PreAuthorize("@authorizationService.canGradeSubmission(#create.submissionId)")
@@ -61,7 +64,7 @@ public class GradeController {
 		gradeService.addGradedFeedback(person.getId(), assignmentId, create);
 
 		return "redirect:/group/{groupId}?assignment={assignmentId}&submission="
-				+ create.getSubmissionId();
+				+ submissionService.getSubmissionIdByCoreId(create.getSubmissionId());
 	}
 
 	@PostMapping("/edition/{editionId}/{assignmentId}")
diff --git a/src/main/java/nl/tudelft/submit/controller/StudentGroupController.java b/src/main/java/nl/tudelft/submit/controller/StudentGroupController.java
index 70f41e031782b41e0fa92af94578874e553d6663..5a9b94344a89b557b4ac7d0ddad516198cffde56 100644
--- a/src/main/java/nl/tudelft/submit/controller/StudentGroupController.java
+++ b/src/main/java/nl/tudelft/submit/controller/StudentGroupController.java
@@ -17,6 +17,7 @@
  */
 package nl.tudelft.submit.controller;
 
+import java.io.IOException;
 import java.time.LocalDateTime;
 import java.util.*;
 import java.util.function.Function;
@@ -30,6 +31,10 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Controller;
 import org.springframework.ui.Model;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+import com.opencsv.exceptions.CsvValidationException;
 
 import nl.tudelft.labracore.api.dto.*;
 import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
@@ -41,6 +46,7 @@ import nl.tudelft.submit.dto.create.note.GroupNoteCreateDTO;
 import nl.tudelft.submit.dto.create.note.SubmissionNoteCreateDTO;
 import nl.tudelft.submit.dto.id.SubmissionId;
 import nl.tudelft.submit.dto.view.labracore.SubmitGroupDetailsDTO;
+import nl.tudelft.submit.exception.GroupSwitchingException;
 import nl.tudelft.submit.model.Signature;
 import nl.tudelft.submit.security.AuthorizationService;
 import nl.tudelft.submit.service.*;
@@ -182,10 +188,16 @@ public class StudentGroupController {
 	@PostMapping
 	@PreAuthorize("@authorizationService.canCreateGroup(#create.getModule().getId())")
 	public String createGroup(@AuthenticatedPerson Person person,
-			@ModelAttribute("groupCreate") StudentGroupCreateDTO create) {
-		Long id = groupService.createGroup(create);
-
-		return "redirect:/group/" + id;
+			@ModelAttribute("groupCreate") StudentGroupCreateDTO create,
+			RedirectAttributes redirectAttributes) {
+		try {
+			Long id = groupService.createGroup(create);
+			return "redirect:/group/" + id;
+		} catch (GroupSwitchingException e) {
+			redirectAttributes.addFlashAttribute("error", e.getMessage());
+
+			return "redirect:/module/" + create.getModule().getId() + "/groups";
+		}
 	}
 
 	/**
@@ -221,6 +233,21 @@ public class StudentGroupController {
 		return "redirect:/module/" + moduleId + "/groups";
 	}
 
+	/**
+	 * Imports student groups from a csv file.
+	 *
+	 * @param  moduleId the id of the module to which these groups will belong
+	 * @param  file     the csv file containing the groups
+	 * @return          a redirect to getGroupsPerModule()
+	 */
+	@PostMapping("{moduleId}/import")
+	@PreAuthorize("@authorizationService.canImportStudentGroups(#moduleId)")
+	public String importStudentGroups(@PathVariable Long moduleId,
+			@RequestParam("file") MultipartFile file) throws IOException, CsvValidationException {
+		groupService.importStudentGroups(moduleId, file);
+		return "redirect:/module/" + moduleId + "/groups";
+	}
+
 	/**
 	 * Edits group capacity for a module.
 	 *
@@ -246,10 +273,18 @@ public class StudentGroupController {
 	 */
 	@PostMapping("/{id}/join")
 	@PreAuthorize("@authorizationService.canJoinGroup(#id)")
-	public String joinGroup(@AuthenticatedPerson Person person, @PathVariable Long id) {
-		groupService.addPeopleToGroup(id, List.of(person.getUsername()));
-
+	public String joinGroup(@AuthenticatedPerson Person person, @PathVariable Long id,
+			RedirectAttributes redirectAttributes) {
 		ModuleDetailsDTO module = moduleService.getModuleById(groupService.getModuleIdFromGroup(id));
+		try {
+			groupService.addPeopleToGroup(id, List.of(person.getUsername()));
+
+		} catch (GroupSwitchingException e) {
+			redirectAttributes.addFlashAttribute("error", e.getMessage());
+
+			return "redirect:/module/" + module.getId() + "/groups";
+		}
+
 		return "redirect:/edition/" + module.getEdition().getId() + "/student?module=" + module.getId();
 	}
 
@@ -262,8 +297,13 @@ public class StudentGroupController {
 	 */
 	@PostMapping("/{id}/members")
 	@PreAuthorize("@authorizationService.canAddMembersToGroup(#id)")
-	public String addMembersToGroup(@PathVariable Long id, @RequestParam List<String> usernames) {
-		groupService.addPeopleToGroup(id, usernames);
+	public String addMembersToGroup(@PathVariable Long id, @RequestParam List<String> usernames,
+			RedirectAttributes redirectAttributes) {
+		try {
+			groupService.addPeopleToGroup(id, usernames);
+		} catch (GroupSwitchingException e) {
+			redirectAttributes.addFlashAttribute("error", e.getMessage());
+		}
 
 		return "redirect:/group/" + id;
 	}
diff --git a/src/main/java/nl/tudelft/submit/controller/SubmissionController.java b/src/main/java/nl/tudelft/submit/controller/SubmissionController.java
index d106fb41b6a43745d8a28de5230c61f8e08620da..465558d5c837aa753fc43f22a4227c5c62770289 100644
--- a/src/main/java/nl/tudelft/submit/controller/SubmissionController.java
+++ b/src/main/java/nl/tudelft/submit/controller/SubmissionController.java
@@ -41,6 +41,7 @@ import org.springframework.web.multipart.MultipartHttpServletRequest;
 import nl.tudelft.labracore.api.dto.*;
 import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
 import nl.tudelft.labracore.lib.security.user.Person;
+import nl.tudelft.librador.dto.view.View;
 import nl.tudelft.submit.dto.create.MailMessageCreateDTO;
 import nl.tudelft.submit.dto.create.SubmissionDownloadConfigCreateDTO;
 import nl.tudelft.submit.dto.create.note.SubmissionNoteCreateDTO;
@@ -48,7 +49,9 @@ import nl.tudelft.submit.dto.view.FileViewDTO;
 import nl.tudelft.submit.dto.view.VersionViewDTO;
 import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentDetailsDTO;
 import nl.tudelft.submit.exception.DisallowedUploadException;
+import nl.tudelft.submit.model.CoreSubmission;
 import nl.tudelft.submit.model.Signature;
+import nl.tudelft.submit.model.TestSubmission;
 import nl.tudelft.submit.security.AuthorizationService;
 import nl.tudelft.submit.service.*;
 
@@ -98,6 +101,48 @@ public class SubmissionController {
 	@Autowired
 	private AuthorizationService authorizationService;
 
+	/**
+	 * Adds and uploads a newly created test submission.
+	 *
+	 * @param  assignmentId the assignment id
+	 * @param  versionId    the id of the version to run the test submission on
+	 * @param  request      the request
+	 * @return              redirection to the assignment page
+	 */
+	@Validated
+	@PostMapping(value = "/test-submission/{assignmentId}", consumes = { "multipart/form-data" })
+	@PreAuthorize("@authorizationService.canSubmitTestSubmission(#assignmentId)")
+	public String addTestSubmission(@PathVariable Long assignmentId,
+			@RequestParam(name = "versionId", required = false) Long versionId,
+			MultipartHttpServletRequest request) {
+		AssignmentDetailsDTO assignment = assignmentService.getAssignmentDetails(assignmentId);
+		String submissionsDirectory = storageDir + "/submissions";
+
+		try {
+			if (!submissionService.checkSubmission(assignment.getAllowedFileTypes(), request.getFileMap()))
+				throw new DisallowedUploadException("One or more of the submitted files are not allowed");
+
+			TestSubmission submission = submissionService
+					.createTestSubmissionForAssignment(assignment.getId());
+
+			fileService.uploadFiles(request.getFileMap(), submission.getId(), submissionsDirectory);
+
+			VersionViewDTO version;
+			if (versionId == null) {
+				version = versionService.getVersionsPerAssignment(assignmentId).get(0);
+			} else {
+				version = View.convert(versionService.getVersion(versionId), VersionViewDTO.class);
+			}
+			if (version != null && version.getScriptTrain() != null) {
+				Long trainId = version.getScriptTrain().getId();
+				scriptService.runScriptTrain(trainId, submission);
+			}
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		return "redirect:/assignment/" + assignmentId;
+	}
+
 	/**
 	 * Adds and uploads a newly created submission.
 	 *
@@ -129,13 +174,14 @@ public class SubmissionController {
 				throw new DisallowedUploadException("One or more of the submitted files are not allowed");
 
 			Long id = submissionService.addSubmission(createSubmissionDto);
+			CoreSubmission submission = submissionService.getOrCreateSubmitCoreSubmission(id);
 
-			fileService.uploadFiles(request.getFileMap(), id, submissionsDirectory);
+			fileService.uploadFiles(request.getFileMap(), submission.getId(), submissionsDirectory);
 
 			VersionViewDTO version = versionService.getVersionOfAssignmentForGroup(assignment.getId(),
 					group.getId());
 			if (version != null && version.getScriptTrain() != null) {
-				scriptService.runScriptTrain(version.getScriptTrain().getId(), id);
+				scriptService.runScriptTrain(version.getScriptTrain().getId(), submission);
 			}
 
 			emailService.sendEmailToAuthPerson(
@@ -192,7 +238,8 @@ public class SubmissionController {
 	public ResponseEntity<Resource> downloadFiles(@PathVariable Long submissionId) throws Exception {
 		String submissionsDirectory = storageDir + "/submissions";
 		String downloadsDirectory = tempStorageDir + "/downloads";
-		Resource resource = fileService.downloadFiles(submissionId, submissionsDirectory, downloadsDirectory);
+		Resource resource = fileService.downloadFiles(submissionService.getSubmissionIdByCoreId(submissionId),
+				submissionsDirectory, downloadsDirectory);
 		return ResponseEntity.ok()
 				.contentType(MediaType.parseMediaType(MediaType.APPLICATION_OCTET_STREAM_VALUE))
 				.header(HttpHeaders.CONTENT_DISPOSITION,
@@ -343,7 +390,8 @@ public class SubmissionController {
 		VersionViewDTO version = versionService.getVersionOfAssignmentForGroup(assignmentId, groupId);
 
 		if (version != null && version.getScriptTrain() != null) {
-			scriptService.runScriptTrain(version.getScriptTrain().getId(), id);
+			scriptService.runScriptTrain(version.getScriptTrain().getId(),
+					submissionService.getOrCreateSubmitCoreSubmission(id));
 		}
 
 		return "resubmitted";
diff --git a/src/main/java/nl/tudelft/submit/csv/CSVService.java b/src/main/java/nl/tudelft/submit/csv/CSVService.java
index 642433207272449581d60126bd610c90b4d1d08c..b19933291e6a507a0f011ee65d3dbb1b8852498f 100644
--- a/src/main/java/nl/tudelft/submit/csv/CSVService.java
+++ b/src/main/java/nl/tudelft/submit/csv/CSVService.java
@@ -17,9 +17,7 @@
  */
 package nl.tudelft.submit.csv;
 
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.Reader;
+import java.io.*;
 import java.net.MalformedURLException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
diff --git a/src/main/java/nl/tudelft/submit/dto/create/SubmitAssignmentCreateDTO.java b/src/main/java/nl/tudelft/submit/dto/create/SubmitAssignmentCreateDTO.java
index d9af50c9e67e9b572f7f3f2dd9913b148c20508c..921eafef4c9cc5df6917bef21f8976ab74dbe5e9 100644
--- a/src/main/java/nl/tudelft/submit/dto/create/SubmitAssignmentCreateDTO.java
+++ b/src/main/java/nl/tudelft/submit/dto/create/SubmitAssignmentCreateDTO.java
@@ -24,10 +24,7 @@ import javax.validation.constraints.NotNull;
 
 import org.springframework.format.annotation.DateTimeFormat;
 
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.NoArgsConstructor;
+import lombok.*;
 import nl.tudelft.labracore.api.dto.Description;
 import nl.tudelft.labracore.api.dto.ModuleIdDTO;
 import nl.tudelft.labracore.lib.api.GradeScheme;
@@ -63,6 +60,8 @@ public class SubmitAssignmentCreateDTO extends Create<SubmitAssignment> {
 
 	private List<String> allowedFileTypes = null;
 
+	private Boolean hidden = false;
+
 	@NotNull
 	private ModuleIdDTO module;
 
diff --git a/src/main/java/nl/tudelft/submit/dto/view/labracore/SubmitAssignmentDetailsDTO.java b/src/main/java/nl/tudelft/submit/dto/view/labracore/SubmitAssignmentDetailsDTO.java
index bb2f96dc8c15684d6fe632c7a2cb9563ec27beb6..ba04c2fc0796ff429606c1190082b4f52a6f61e5 100644
--- a/src/main/java/nl/tudelft/submit/dto/view/labracore/SubmitAssignmentDetailsDTO.java
+++ b/src/main/java/nl/tudelft/submit/dto/view/labracore/SubmitAssignmentDetailsDTO.java
@@ -57,6 +57,8 @@ public class SubmitAssignmentDetailsDTO {
 
 	private ModuleLayer1DTO module;
 
+	private Boolean hidden;
+
 	private List<SubmissionSummaryDTO> submissions;
 
 }
diff --git a/src/main/java/nl/tudelft/submit/dto/view/labracore/SubmitAssignmentSummaryDTO.java b/src/main/java/nl/tudelft/submit/dto/view/labracore/SubmitAssignmentSummaryDTO.java
index cebb589587f806b5a9fcd40726482adfa9ad7428..895e49df4d2d6e053fc54e0a0e19f91561933bd0 100644
--- a/src/main/java/nl/tudelft/submit/dto/view/labracore/SubmitAssignmentSummaryDTO.java
+++ b/src/main/java/nl/tudelft/submit/dto/view/labracore/SubmitAssignmentSummaryDTO.java
@@ -43,5 +43,6 @@ public class SubmitAssignmentSummaryDTO extends WithNoteViewDTO<AssignmentSummar
 	private String approveScript;
 	private Integer submissionLimit;
 	private List<String> allowedFileTypes;
+	private Boolean hidden = true;
 
 }
diff --git a/src/main/java/nl/tudelft/submit/enums/TokenType.java b/src/main/java/nl/tudelft/submit/enums/TokenType.java
index 29ff4b2b3f0af560ef2285772bee55da95298a40..2c9ea4b9518fd4158faa7ef680ac1f7f24b7aaae 100644
--- a/src/main/java/nl/tudelft/submit/enums/TokenType.java
+++ b/src/main/java/nl/tudelft/submit/enums/TokenType.java
@@ -83,7 +83,7 @@ public enum TokenType {
 		"abs|max|min|sqrt|avg|clamp"
 	),
 	VARIABLE(
-		"[A-Za-z_][A-Za-z0-9_]*"
+		"[A-Za-z0-9_]*[A-Za-z_][A-Za-z0-9_]*"
 	);
 
 	private Pattern pattern;
diff --git a/src/main/java/nl/tudelft/submit/model/SubmitSubmission.java b/src/main/java/nl/tudelft/submit/model/AbstractSubmitSubmission.java
similarity index 84%
rename from src/main/java/nl/tudelft/submit/model/SubmitSubmission.java
rename to src/main/java/nl/tudelft/submit/model/AbstractSubmitSubmission.java
index d51c74dbcf5fcd1afa49f41d0957e64173628885..ad8ef14a30aa01b00712f91ce716fdae879931a4 100644
--- a/src/main/java/nl/tudelft/submit/model/SubmitSubmission.java
+++ b/src/main/java/nl/tudelft/submit/model/AbstractSubmitSubmission.java
@@ -20,22 +20,23 @@ package nl.tudelft.submit.model;
 import java.util.ArrayList;
 import java.util.List;
 
-import javax.persistence.Entity;
-import javax.persistence.Id;
-import javax.persistence.OneToMany;
+import javax.persistence.*;
 import javax.validation.constraints.NotNull;
 
 import lombok.*;
+import lombok.experimental.SuperBuilder;
 import nl.tudelft.submit.model.script.ScriptTrainResult;
 
 @Data
 @Entity
-@Builder
+@SuperBuilder
 @NoArgsConstructor
 @AllArgsConstructor
-public class SubmitSubmission {
+@Inheritance(strategy = InheritanceType.JOINED)
+public abstract class AbstractSubmitSubmission {
 
 	@Id
+	@GeneratedValue(strategy = GenerationType.IDENTITY)
 	private Long id;
 
 	@NotNull
diff --git a/src/main/java/nl/tudelft/submit/model/CoreSubmission.java b/src/main/java/nl/tudelft/submit/model/CoreSubmission.java
new file mode 100644
index 0000000000000000000000000000000000000000..515764b5c23f21d7eff67882ee155a9d8ce99c04
--- /dev/null
+++ b/src/main/java/nl/tudelft/submit/model/CoreSubmission.java
@@ -0,0 +1,36 @@
+/*
+ * Submit
+ * Copyright (C) 2020 - Delft University of Technology
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.submit.model;
+
+import javax.persistence.Entity;
+
+import lombok.*;
+import lombok.experimental.SuperBuilder;
+
+@Data
+@Entity
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+@ToString(callSuper = true)
+@EqualsAndHashCode(callSuper = true)
+public class CoreSubmission extends AbstractSubmitSubmission {
+
+	private Long coreSubmissionId;
+
+}
diff --git a/src/main/java/nl/tudelft/submit/model/SubmitAssignment.java b/src/main/java/nl/tudelft/submit/model/SubmitAssignment.java
index 669dd8da5110a78b42ff9c4fa403d63e77a3d513..11371d4bb4f650b3d10f304faaca8a2e65626925 100644
--- a/src/main/java/nl/tudelft/submit/model/SubmitAssignment.java
+++ b/src/main/java/nl/tudelft/submit/model/SubmitAssignment.java
@@ -20,6 +20,7 @@ package nl.tudelft.submit.model;
 import javax.persistence.Entity;
 import javax.persistence.Id;
 import javax.persistence.Lob;
+import javax.validation.constraints.NotNull;
 
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -42,4 +43,8 @@ public class SubmitAssignment {
 	@Lob
 	private String guidelines;
 
+	@NotNull
+	@Builder.Default
+	private Boolean hidden = true;
+
 }
diff --git a/src/main/java/nl/tudelft/submit/model/TestSubmission.java b/src/main/java/nl/tudelft/submit/model/TestSubmission.java
new file mode 100644
index 0000000000000000000000000000000000000000..6188f40eccd31f0ab4ab4589dbecc10ddbd675d5
--- /dev/null
+++ b/src/main/java/nl/tudelft/submit/model/TestSubmission.java
@@ -0,0 +1,43 @@
+/*
+ * Submit
+ * Copyright (C) 2020 - Delft University of Technology
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.submit.model;
+
+import java.time.LocalDateTime;
+
+import javax.persistence.Entity;
+import javax.validation.constraints.NotNull;
+
+import lombok.*;
+import lombok.experimental.SuperBuilder;
+
+@Data
+@Entity
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+@ToString(callSuper = true)
+@EqualsAndHashCode(callSuper = true)
+public class TestSubmission extends AbstractSubmitSubmission {
+
+	private Long assignmentId;
+
+	@NotNull
+	@Builder.Default
+	private LocalDateTime createdDate = LocalDateTime.now();
+
+}
diff --git a/src/main/java/nl/tudelft/submit/model/grading/GradingFormula.java b/src/main/java/nl/tudelft/submit/model/grading/GradingFormula.java
index f4b1678fa989f10204af04ec315adf96bac5166a..65e31bdf0c06b15515efddd7d8d525787ebf3753 100644
--- a/src/main/java/nl/tudelft/submit/model/grading/GradingFormula.java
+++ b/src/main/java/nl/tudelft/submit/model/grading/GradingFormula.java
@@ -20,6 +20,7 @@ package nl.tudelft.submit.model.grading;
 import java.io.Serializable;
 
 import javax.persistence.EmbeddedId;
+import javax.persistence.Lob;
 import javax.persistence.MappedSuperclass;
 import javax.validation.constraints.NotBlank;
 import javax.validation.constraints.NotNull;
@@ -44,6 +45,7 @@ public abstract class GradingFormula<E extends Serializable> {
 	private E entityId;
 
 	@NotBlank
+	@Lob
 	private String formula;
 
 	@NotNull
diff --git a/src/main/java/nl/tudelft/submit/model/script/AbstractScriptResult.java b/src/main/java/nl/tudelft/submit/model/script/AbstractScriptResult.java
index dae964779d37227a3f5f758d45415515836ef69b..94db51a63dfeb1f95b10c46b8f85e9fb37ea557b 100644
--- a/src/main/java/nl/tudelft/submit/model/script/AbstractScriptResult.java
+++ b/src/main/java/nl/tudelft/submit/model/script/AbstractScriptResult.java
@@ -23,7 +23,7 @@ import javax.validation.constraints.NotNull;
 import lombok.*;
 import lombok.experimental.SuperBuilder;
 import nl.tudelft.submit.enums.ScriptStatus;
-import nl.tudelft.submit.model.SubmitSubmission;
+import nl.tudelft.submit.model.AbstractSubmitSubmission;
 
 @Data
 @SuperBuilder
@@ -38,7 +38,7 @@ public abstract class AbstractScriptResult {
 
 	@NotNull
 	@ManyToOne
-	private SubmitSubmission submission;
+	private AbstractSubmitSubmission submission;
 
 	@NotNull
 	@Builder.Default
diff --git a/src/main/java/nl/tudelft/submit/repository/AbstractSubmitSubmissionRepository.java b/src/main/java/nl/tudelft/submit/repository/AbstractSubmitSubmissionRepository.java
new file mode 100644
index 0000000000000000000000000000000000000000..b371c68e029d47d5c6bde81c894195433f1fdb65
--- /dev/null
+++ b/src/main/java/nl/tudelft/submit/repository/AbstractSubmitSubmissionRepository.java
@@ -0,0 +1,26 @@
+/*
+ * Submit
+ * Copyright (C) 2020 - Delft University of Technology
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.submit.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import nl.tudelft.submit.model.AbstractSubmitSubmission;
+
+public interface AbstractSubmitSubmissionRepository<T extends AbstractSubmitSubmission>
+		extends JpaRepository<T, Long> {
+}
diff --git a/src/main/java/nl/tudelft/submit/repository/CoreSubmissionRepository.java b/src/main/java/nl/tudelft/submit/repository/CoreSubmissionRepository.java
new file mode 100644
index 0000000000000000000000000000000000000000..e64809e24c8ae0f9a49071f6214cc91e8d4ecf62
--- /dev/null
+++ b/src/main/java/nl/tudelft/submit/repository/CoreSubmissionRepository.java
@@ -0,0 +1,45 @@
+/*
+ * Submit
+ * Copyright (C) 2020 - Delft University of Technology
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.submit.repository;
+
+import java.util.Optional;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.rest.webmvc.ResourceNotFoundException;
+import org.springframework.stereotype.Repository;
+
+import nl.tudelft.submit.model.CoreSubmission;
+
+@Repository
+public interface CoreSubmissionRepository extends JpaRepository<CoreSubmission, Long> {
+
+	Optional<CoreSubmission> findByCoreSubmissionId(Long coreId);
+
+	/**
+	 * Finds the CoreSubmission by id or throws a ResourceNotFoundException.
+	 *
+	 * @param  id the id of the submission
+	 * @return    the CoreSubmission object
+	 */
+	default CoreSubmission findByIdOrThrow(Long id) {
+		return findById(id)
+				.orElseThrow(
+						() -> new ResourceNotFoundException("SubmitCoreSubmission was not found: " + id));
+	}
+
+}
diff --git a/src/main/java/nl/tudelft/submit/repository/SubmitSubmissionRepository.java b/src/main/java/nl/tudelft/submit/repository/TestSubmissionRepository.java
similarity index 68%
rename from src/main/java/nl/tudelft/submit/repository/SubmitSubmissionRepository.java
rename to src/main/java/nl/tudelft/submit/repository/TestSubmissionRepository.java
index cfec29eccdac9913952aa1f75db15c0501fb58f1..e6d4593a19a55172d254c71d1c41bf70021ac6f5 100644
--- a/src/main/java/nl/tudelft/submit/repository/SubmitSubmissionRepository.java
+++ b/src/main/java/nl/tudelft/submit/repository/TestSubmissionRepository.java
@@ -17,24 +17,29 @@
  */
 package nl.tudelft.submit.repository;
 
+import java.util.List;
+
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.rest.webmvc.ResourceNotFoundException;
 import org.springframework.stereotype.Repository;
 
-import nl.tudelft.submit.model.SubmitSubmission;
+import nl.tudelft.submit.model.TestSubmission;
 
 @Repository
-public interface SubmitSubmissionRepository extends JpaRepository<SubmitSubmission, Long> {
+public interface TestSubmissionRepository extends JpaRepository<TestSubmission, Long> {
+
+	List<TestSubmission> findAllByAssignmentId(Long assignmentId);
 
 	/**
-	 * Finds the SubmitSubmission by id or throws a ResourceNotFoundException.
+	 * Finds the TestSubmission by id or throws a ResourceNotFoundException.
 	 *
 	 * @param  id the id of the submission
-	 * @return    the SubmitSubmission object
+	 * @return    the TestSubmission object
 	 */
-	default SubmitSubmission findByIdOrThrow(Long id) {
+	default TestSubmission findByIdOrThrow(Long id) {
 		return findById(id)
-				.orElseThrow(() -> new ResourceNotFoundException("SubmitSubmission was not found: " + id));
+				.orElseThrow(
+						() -> new ResourceNotFoundException("SubmitTestSubmission was not found: " + id));
 	}
 
 }
diff --git a/src/main/java/nl/tudelft/submit/security/AuthorizationService.java b/src/main/java/nl/tudelft/submit/security/AuthorizationService.java
index 5d11d88b1a6f19b030fcd103c7770b2f68ca5323..e8731f9d1772a6f6c5ad8b19f1868fd8ce373169 100644
--- a/src/main/java/nl/tudelft/submit/security/AuthorizationService.java
+++ b/src/main/java/nl/tudelft/submit/security/AuthorizationService.java
@@ -44,6 +44,7 @@ import nl.tudelft.submit.repository.announcement.EditionAnnouncementRepository;
 import nl.tudelft.submit.repository.script.ScriptRepository;
 import nl.tudelft.submit.repository.script.ScriptTrainRepository;
 import nl.tudelft.submit.repository.script.ScriptWagonRepository;
+import nl.tudelft.submit.service.AssignmentService;
 import nl.tudelft.submit.service.VersionService;
 
 @Service
@@ -68,6 +69,8 @@ public class AuthorizationService {
 	@Autowired
 	private AssignmentCacheManager assignmentCache;
 	@Autowired
+	private AssignmentService assignmentService;
+	@Autowired
 	private SubmissionCacheManager submissionCache;
 	@Autowired
 	private StudentGroupCacheManager groupCache;
@@ -830,7 +833,9 @@ public class AuthorizationService {
 	 * @return              True iff the user can view the assignment
 	 */
 	public boolean canViewAssignment(Long assignmentId) {
-		return isAtLeastStudentInAssignment(assignmentId) || isTeacherROInAssignment(assignmentId);
+		boolean hidden = assignmentService.getSubmitAssignmentDetails(assignmentId).getHidden();
+		return (isAtLeastStudentInAssignment(assignmentId) && !hidden) || canEditAssignment(assignmentId)
+				|| isTeacherROInAssignment(assignmentId);
 	}
 
 	/**
@@ -874,6 +879,16 @@ public class AuthorizationService {
 		return isAtLeastStudentInAssignment(assignmentId) && isMemberOfGroup(groupId);
 	}
 
+	/**
+	 * Checks whether the authenticated user can make a test submission for an assignment.
+	 *
+	 * @param  assignmentId The id of the assignment
+	 * @return              True iff the user can make a test submission for the assignment
+	 */
+	public boolean canSubmitTestSubmission(Long assignmentId) {
+		return isAtLeastHeadTAInAssignment(assignmentId);
+	}
+
 	/**
 	 * Checks whether the authenticated user can create an assignment in a module.
 	 *
@@ -1109,12 +1124,22 @@ public class AuthorizationService {
 	 * Checks whether the authenticated user can create a group in a module.
 	 *
 	 * @param  moduleId The id of the module
-	 * @return          True iff the user can view a group in the module.
+	 * @return          True iff the user can create a group in the module.
 	 */
 	public boolean canCreateGroup(Long moduleId) {
 		return isAtLeastHeadTAInModule(moduleId);
 	}
 
+	/**
+	 * Checks whether the authenticated user can import groups of students.
+	 *
+	 * @param  moduleId The id of the module
+	 * @return          True, iff the user can import groups of students.
+	 */
+	public boolean canImportStudentGroups(Long moduleId) {
+		return isAtLeastHeadTAInModule(moduleId);
+	}
+
 	/**
 	 * Checks whether the authenticated user can edit a group's note.
 	 *
diff --git a/src/main/java/nl/tudelft/submit/service/AssignmentService.java b/src/main/java/nl/tudelft/submit/service/AssignmentService.java
index 971bd68db54d13c27bcc1466515d7f47a517a3b2..6e5e69ca53a279c59b3860b2212ea43fb38ba4b9 100644
--- a/src/main/java/nl/tudelft/submit/service/AssignmentService.java
+++ b/src/main/java/nl/tudelft/submit/service/AssignmentService.java
@@ -25,6 +25,8 @@ import java.util.*;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
+import javax.transaction.Transactional;
+
 import org.modelmapper.ModelMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
@@ -47,6 +49,7 @@ import nl.tudelft.submit.dto.patch.SubmitAssignmentPatchDTO;
 import nl.tudelft.submit.dto.patch.grading.AssignmentGradeImportDTO;
 import nl.tudelft.submit.dto.view.VersionViewDTO;
 import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentDetailsDTO;
+import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentSummaryDTO;
 import nl.tudelft.submit.dto.view.labracore.SubmitGroupSummaryDTO;
 import nl.tudelft.submit.dto.view.labracore.SubmitSubmissionViewDTO;
 import nl.tudelft.submit.dto.view.script.ScriptTrainResultViewDTO;
@@ -54,6 +57,7 @@ import nl.tudelft.submit.model.SubmitAssignment;
 import nl.tudelft.submit.model.note.AssignmentNote;
 import nl.tudelft.submit.repository.SubmitAssignmentRepository;
 import nl.tudelft.submit.repository.note.AssignmentNoteRepository;
+import nl.tudelft.submit.security.AuthorizationService;
 
 @Service
 public class AssignmentService {
@@ -74,6 +78,9 @@ public class AssignmentService {
 	@Autowired
 	private SubmitAssignmentRepository assignmentRepository;
 	@Autowired
+	@Lazy
+	private AuthorizationService authorizationService;
+	@Autowired
 	private AssignmentNoteRepository noteRepository;
 
 	@Autowired
@@ -137,6 +144,7 @@ public class AssignmentService {
 				SubmitAssignmentDetailsDTO.class);
 		details.setScoreType(getOrCreateSubmitAssignment(id).getScoreType());
 		details.setGuidelines(getOrCreateSubmitAssignment(id).getGuidelines());
+		details.setHidden(getOrCreateSubmitAssignment(id).getHidden());
 		return details;
 	}
 
@@ -309,7 +317,8 @@ public class AssignmentService {
 			s.setIsLatest(false);
 			s.setFeedback(feedbackService.getAllFeedbackPerSubmission(s.getId()));
 			s.setScriptResults(
-					View.convert(submissionService.getOrCreateSubmitSubmission(s.getId()).getScriptResults(),
+					View.convert(
+							submissionService.getOrCreateSubmitCoreSubmission(s.getId()).getScriptResults(),
 							ScriptTrainResultViewDTO.class));
 			byGroup.get(s.getGroup().getId()).add(s);
 		});
@@ -335,7 +344,8 @@ public class AssignmentService {
 		submissions.forEach(s -> {
 			s.setFeedback(feedbackService.getAllFeedbackPerSubmission(s.getId()));
 			s.setScriptResults(
-					View.convert(submissionService.getOrCreateSubmitSubmission(s.getId()).getScriptResults(),
+					View.convert(
+							submissionService.getOrCreateSubmitCoreSubmission(s.getId()).getScriptResults(),
 							ScriptTrainResultViewDTO.class));
 		});
 		return submissions;
@@ -405,4 +415,26 @@ public class AssignmentService {
 		return files != null && files.length > 0;
 	}
 
+	/**
+	 * Toggles the hidden state of an assignment.
+	 *
+	 * @param id The id of the assignment.
+	 */
+	@Transactional
+	public void toggleHide(Long id) {
+		SubmitAssignment assignment = getOrCreateSubmitAssignment(id);
+		assignment.setHidden(!assignment.getHidden());
+		assignmentRepository.save(assignment);
+	}
+
+	/**
+	 * Gets/Filters the assignments that are visible to the user
+	 *
+	 * @param  assignments the assignments to filter on
+	 * @return             the visible/filtered assignments
+	 */
+	public List<SubmitAssignmentSummaryDTO> getVisibleAssignments(
+			List<SubmitAssignmentSummaryDTO> assignments) {
+		return assignments.stream().filter(x -> authorizationService.canViewAssignment(x.getId())).toList();
+	}
 }
diff --git a/src/main/java/nl/tudelft/submit/service/EditionService.java b/src/main/java/nl/tudelft/submit/service/EditionService.java
index a6662baaf32b81851902d267a2e7b12871753f01..1fdf2e368a50097f64cb633ce90c3adb79f5f225 100644
--- a/src/main/java/nl/tudelft/submit/service/EditionService.java
+++ b/src/main/java/nl/tudelft/submit/service/EditionService.java
@@ -473,6 +473,7 @@ public class EditionService {
 	 *
 	 * @param id The id of the edition
 	 */
+	@Transactional
 	public void toggleHide(Long id) {
 		SubmitEdition edition = getOrCreateSubmitEdition(id);
 		edition.setHidden(!edition.getHidden());
diff --git a/src/main/java/nl/tudelft/submit/service/GradeService.java b/src/main/java/nl/tudelft/submit/service/GradeService.java
index fb39593d3c82c9e6ebf64867e55d5cd460a315aa..7e1a6f740261fb699b2bb5948796b9d86bc728d9 100644
--- a/src/main/java/nl/tudelft/submit/service/GradeService.java
+++ b/src/main/java/nl/tudelft/submit/service/GradeService.java
@@ -30,7 +30,6 @@ import java.util.stream.Collectors;
 
 import javax.transaction.Transactional;
 
-import org.modelmapper.ModelMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Lazy;
@@ -72,8 +71,6 @@ public class GradeService {
 		GRADE_FORMAT_100.setRoundingMode(RoundingMode.HALF_UP);
 	}
 
-	private ModelMapper mapper = new ModelMapper();
-
 	@Value("${labrador.api.url}")
 	private String domain;
 
@@ -118,6 +115,8 @@ public class GradeService {
 	@Autowired
 	private AssignmentService assignmentService;
 	@Autowired
+	private SubmissionService submissionService;
+	@Autowired
 	private UserSettingsService userSettingsService;
 	@Autowired
 	private NotificationService notificationService;
@@ -804,7 +803,8 @@ public class GradeService {
 			}
 
 			gradeId = addGrade(create.getGradeCreateDTO(gradeType));
-			updateGradesAfterGradedSubmission(create.getSubmissionId());
+			updateGradesAfterGradedSubmission(
+					submissionService.getSubmissionIdByCoreId(create.getSubmissionId()));
 		}
 		feedbackService.addFeedback(create.getFeedbackCreateDTO(gradeId));
 	}
diff --git a/src/main/java/nl/tudelft/submit/service/ScriptService.java b/src/main/java/nl/tudelft/submit/service/ScriptService.java
index 0920565025b873fa07afd7b7e76fbac0f3de0d2a..86697d2f0adffca56498c0ed833dd63230af3dea 100644
--- a/src/main/java/nl/tudelft/submit/service/ScriptService.java
+++ b/src/main/java/nl/tudelft/submit/service/ScriptService.java
@@ -62,7 +62,8 @@ import nl.tudelft.submit.dto.patch.script.ScriptWagonPatchDTO;
 import nl.tudelft.submit.dto.view.script.ScriptWagonViewDTO;
 import nl.tudelft.submit.enums.NotificationPreference;
 import nl.tudelft.submit.enums.ScriptStatus;
-import nl.tudelft.submit.model.SubmitSubmission;
+import nl.tudelft.submit.model.AbstractSubmitSubmission;
+import nl.tudelft.submit.model.CoreSubmission;
 import nl.tudelft.submit.model.Version;
 import nl.tudelft.submit.model.script.*;
 import nl.tudelft.submit.repository.SubmissionKeyRepository;
@@ -545,15 +546,13 @@ public class ScriptService {
 	/**
 	 * Runs the script train for a submission.
 	 *
-	 * @param trainId      The id of the script train
-	 * @param submissionId The id of the submission
+	 * @param trainId    The id of the script train
+	 * @param submission The submission to run the script on
 	 */
 	@Transactional
-	public void runScriptTrain(Long trainId, Long submissionId) {
-		submissionService.setPendingState(submissionId, true);
-
+	public void runScriptTrain(Long trainId, AbstractSubmitSubmission submission) {
+		submissionService.setPendingState(submission.getId(), true);
 		ScriptTrain train = getScriptTrainById(trainId);
-		SubmitSubmission submission = submissionService.getOrCreateSubmitSubmission(submissionId);
 
 		scriptTrainResultRepository.save(ScriptTrainResult.builder().train(train).submission(submission)
 				.status(ScriptStatus.RUNNING).build());
@@ -578,15 +577,13 @@ public class ScriptService {
 	 * @param submission The submission
 	 */
 	@Transactional
-	public void runScriptWagon(ScriptWagon wagon, SubmitSubmission submission) {
+	public void runScriptWagon(ScriptWagon wagon, AbstractSubmitSubmission submission) {
 		ScriptWagonResult result = scriptWagonResultRepository.findByWagonIdAndSubmissionId(wagon.getId(),
 				submission.getId());
 		result.setStatus(ScriptStatus.RUNNING);
 		scriptWagonResultRepository.save(result);
 
-		Iterator<Script> scripts = wagon.getScripts().iterator();
-		while (scripts.hasNext()) {
-			Script script = scripts.next();
+		for (Script script : wagon.getScripts()) {
 			runScript(script, submission);
 		}
 	}
@@ -599,7 +596,7 @@ public class ScriptService {
 	 */
 	@Async
 	@Transactional
-	public CompletableFuture<Void> runScript(Script script, SubmitSubmission submission) {
+	public CompletableFuture<Void> runScript(Script script, AbstractSubmitSubmission submission) {
 		ScriptResult result = scriptResultRepository.findByScriptIdAndSubmissionId(script.getId(),
 				submission.getId());
 		result.setStatus(ScriptStatus.RUNNING);
@@ -625,7 +622,7 @@ public class ScriptService {
 	 * @param wagonId    The id of the wagon
 	 * @param submission The submission
 	 */
-	private void checkAndUpdateWagon(Long wagonId, SubmitSubmission submission) {
+	private void checkAndUpdateWagon(Long wagonId, AbstractSubmitSubmission submission) {
 		ScriptWagonResult result = scriptWagonResultRepository.findByWagonIdAndSubmissionId(wagonId,
 				submission.getId());
 		List<ScriptResult> scriptResults = scriptResultRepository
@@ -692,13 +689,17 @@ public class ScriptService {
 			calculateTrainResult(result, wagonResults);
 
 			submissionService.setPendingState(submissionId, false);
-			gradeService.addGrade(new GradeCreateDTO()
-					.scheme(GradeCreateDTO.SchemeEnum.valueOf(result.getTrain().getGradeScheme().name()))
-					.score(result.getScore())
-					.gradedAt(LocalDateTime.now())
-					.isScriptGraded(true)
-					.submission(new SubmissionIdDTO().id(submissionId)));
-			gradeService.updateGradesAfterGradedSubmission(submissionId);
+			Optional<CoreSubmission> submission = submissionService.getCoreSubBySubmitSubId(submissionId);
+			if (submission.isPresent()) {
+				Long coreSubmissionId = submission.get().getCoreSubmissionId();
+				gradeService.addGrade(new GradeCreateDTO()
+						.scheme(GradeCreateDTO.SchemeEnum.valueOf(result.getTrain().getGradeScheme().name()))
+						.score(result.getScore())
+						.gradedAt(LocalDateTime.now())
+						.isScriptGraded(true)
+						.submission(new SubmissionIdDTO().id(coreSubmissionId)));
+				gradeService.updateGradesAfterGradedSubmission(coreSubmissionId);
+			}
 		}
 	}
 
@@ -782,7 +783,7 @@ public class ScriptService {
 					.result(result).build());
 			scriptResultRepository.save(result);
 			checkAndUpdateWagon(result.getScript().getWagon().getId(),
-					submissionService.getOrCreateSubmitSubmission(submissionId));
+					submissionService.getOrCreateSubmitCoreSubmission(submissionId));
 		}
 	}
 
@@ -820,7 +821,7 @@ public class ScriptService {
 
 		if (result.getStatus().isFinished()) {
 			checkAndUpdateWagon(result.getScript().getWagon().getId(),
-					submissionService.getOrCreateSubmitSubmission(feedback.getSubmissionId()));
+					submissionService.getSubmitSubmissionById(feedback.getSubmissionId()));
 		}
 
 	}
diff --git a/src/main/java/nl/tudelft/submit/service/StudentGroupService.java b/src/main/java/nl/tudelft/submit/service/StudentGroupService.java
index b9b49438bd5bc39166090ca7a22d837f5fbccaa0..6c15fe42efeef2c2517a5ef75c7dafe0a02cef86 100644
--- a/src/main/java/nl/tudelft/submit/service/StudentGroupService.java
+++ b/src/main/java/nl/tudelft/submit/service/StudentGroupService.java
@@ -20,6 +20,9 @@ package nl.tudelft.submit.service;
 import static nl.tudelft.submit.enums.SwitchAction.JOIN;
 import static nl.tudelft.submit.enums.SwitchAction.LEAVE;
 
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
 import java.util.*;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -30,13 +33,17 @@ import javax.transaction.Transactional;
 import org.modelmapper.ModelMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.reactive.function.client.WebClientResponseException;
 
+import com.opencsv.exceptions.CsvValidationException;
+
 import nl.tudelft.labracore.api.StudentGroupControllerApi;
 import nl.tudelft.labracore.api.dto.*;
 import nl.tudelft.librador.dto.view.View;
 import nl.tudelft.submit.cache.PersonCacheManager;
 import nl.tudelft.submit.cache.StudentGroupCacheManager;
+import nl.tudelft.submit.csv.CSVService;
 import nl.tudelft.submit.dto.id.GroupId;
 import nl.tudelft.submit.dto.view.labracore.SubmitGroupDetailsDTO;
 import nl.tudelft.submit.dto.view.labracore.SubmitGroupSummaryDTO;
@@ -47,7 +54,6 @@ import nl.tudelft.submit.model.Version;
 import nl.tudelft.submit.repository.SubmitGroupRepository;
 import nl.tudelft.submit.repository.SwitchEventRepository;
 import nl.tudelft.submit.repository.note.GroupNoteRepository;
-import nl.tudelft.submit.repository.note.SubmissionNoteRepository;
 import reactor.core.publisher.Mono;
 
 @Service
@@ -67,17 +73,16 @@ public class StudentGroupService {
 	private SwitchEventRepository switchEventRepository;
 	@Autowired
 	private GroupNoteRepository groupNoteRepository;
-	@Autowired
-	private SubmissionNoteRepository submissionNoteRepository;
 
 	@Autowired
 	private ModuleDivisionService moduleDivisionService;
 	@Autowired
-	private AssignmentService assignmentService;
+	private SubmissionService submissionService;
 	@Autowired
-	private FeedbackService feedbackService;
+	private ModuleService moduleService;
+
 	@Autowired
-	private SubmissionService submissionService;
+	private CSVService csvService;
 
 	private ModelMapper mapper = new ModelMapper();
 
@@ -171,13 +176,22 @@ public class StudentGroupService {
 	}
 
 	/**
-	 * Creates a student group.
+	 * Creates a student group. Throws GroupSwitchingException if a member is already in a group
 	 *
 	 * @param  create The dto to create the group
 	 * @return        The created group's id
 	 */
 	@Transactional
 	public Long createGroup(StudentGroupCreateDTO create) {
+		if (create.getMembers() != null && !create.getMembers().isEmpty()) {
+			List<String> usernames = create.getMembers().stream().map(x -> x.getId().getPersonId())
+					.map(id -> personCache.get(id).orElseThrow().getUsername()).toList();
+			Long moduleId = create.getModule().getId();
+
+			// will throw GroupSwitchingException if a member is already in a group
+			arePeopleAlreadyInAGroup(moduleId, usernames, "Group cannot be created");
+		}
+
 		Long id = groupApi.addGroup(create).block();
 		groupRepository.save(SubmitGroup.builder().id(id).build());
 		return id;
@@ -213,9 +227,34 @@ public class StudentGroupService {
 		groupApi.allocateGroupsForModule(capacity, moduleId).block();
 	}
 
+	/**
+	 * Imports student groups from a csv file. Group members must already be a part of the edition.
+	 *
+	 * @param moduleId the id of the module to import for
+	 * @param file     the csv file
+	 */
+	@Transactional
+	public void importStudentGroups(Long moduleId, MultipartFile file)
+			throws IOException, CsvValidationException {
+		Map<String, List<String>> groupAssignment = new HashMap<>();
+		csvService.readCSV(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8), false, 1)
+				.forEach(line -> groupAssignment
+						.computeIfAbsent(line[1], key -> new ArrayList<>())
+						.add(line[0]));
+
+		for (Map.Entry<String, List<String>> group : groupAssignment.entrySet()) {
+			createGroup(new StudentGroupCreateDTO().members(group.getValue().stream()
+					.map(x -> new RoleIdDTO()
+							.id(new Id().editionId(moduleService.getModuleById(moduleId).getEdition().getId())
+									.personId(personCache.get(x).getId())))
+					.toList()).name(group.getKey())
+					.module(new ModuleIdDTO().id(moduleId)).capacity(group.getValue().size()));
+		}
+	}
+
 	/**
 	 * Attempts to add a person to a group and, if successful, creates a switching event if the group is
-	 * locked.
+	 * locked. Throws GroupSwitchingException if is already in a group in the module.
 	 *
 	 * @param  personId the id of the role of the user in the edition
 	 * @param  groupId  the id of the group to join
@@ -223,6 +262,11 @@ public class StudentGroupService {
 	 */
 	@Transactional
 	public Long addPersonToGroup(Long personId, Long groupId) {
+		Long moduleId = getGroupDetails(groupId).getModule().getId();
+
+		// will throw a GroupSwitchingException if person is already in a group
+		checkIfPersonIsAlreadyInAGroup(personId, moduleId, "Cannot add person to group");
+
 		groupApi.addMemberToGroup(groupId, personId).block();
 
 		if (groupApi.isGroupLockedAndRoleStudent(personId, groupId).block()) {
@@ -233,12 +277,19 @@ public class StudentGroupService {
 	}
 
 	/**
-	 * Adds multiple people to a group if the group is not yet locked.
+	 * Adds multiple people to a group if the group is not yet locked. Throws GroupSwitchingException if at
+	 * least one person in the group is already in a group.
 	 *
 	 * @param id        the id of the group
 	 * @param usernames the list of usernames of the people to be added
 	 */
-	public void addPeopleToGroup(Long id, List<String> usernames) {
+	@Transactional
+	public void addPeopleToGroup(Long id, List<String> usernames) throws GroupSwitchingException {
+		Long moduleId = getGroupDetails(id).getModule().getId();
+
+		// Will throw GroupSwitchingException if at least one of the people are already in a group
+		arePeopleAlreadyInAGroup(moduleId, usernames, "Cannot add person to group");
+
 		if (groupApi.isGroupLocked(id).block()) {
 			if (usernames.size() == 1) {
 				PersonSummaryDTO person = personCache.get(usernames.get(0));
@@ -248,10 +299,45 @@ public class StudentGroupService {
 				throw new GroupSwitchingException("Cannot import multiple people into locked group.");
 			}
 		}
-
 		groupApi.addGroupMembers(id, usernames).block();
 	}
 
+	/**
+	 * Checks if a person is already in a group in a given module. Throws GroupSwitchingException if there is
+	 * at least one person that is already in a group
+	 *
+	 * @param personId the id of the person
+	 * @param moduleId the id of the module
+	 */
+	private void checkIfPersonIsAlreadyInAGroup(Long personId, Long moduleId, String message)
+			throws GroupSwitchingException {
+		Optional<StudentGroupDetailsDTO> currentGroup = getGroupForPersonInModule(personId, moduleId);
+
+		// person is not in any other groups in the module
+		if (currentGroup.isEmpty()) {
+			return;
+		}
+
+		String personName = personCache.get(personId).orElseThrow().getDisplayName();
+		String currentGroupName = currentGroup.get().getName();
+		throw new GroupSwitchingException(
+				message + " because " + personName + " is already in " + currentGroupName);
+	}
+
+	/**
+	 * Checks if at least one person in the given list is already a part of a group in the module
+	 *
+	 * @param usernames list of usernames to be checked
+	 * @param moduleId  the id of the module
+	 *
+	 */
+	private void arePeopleAlreadyInAGroup(Long moduleId, List<String> usernames, String message) {
+		for (String name : usernames) {
+			Long id = personCache.get(name).getId();
+			checkIfPersonIsAlreadyInAGroup(id, moduleId, message);
+		}
+	}
+
 	/**
 	 * Attempts to remove a person from a group and, if successful, creates a switching event if the group is
 	 * locked.
diff --git a/src/main/java/nl/tudelft/submit/service/SubmissionService.java b/src/main/java/nl/tudelft/submit/service/SubmissionService.java
index f736e49752940abe55de67def2f028503b321914..7ee247d156d9461402e1d8dad5224fcca7b1f2f0 100644
--- a/src/main/java/nl/tudelft/submit/service/SubmissionService.java
+++ b/src/main/java/nl/tudelft/submit/service/SubmissionService.java
@@ -50,9 +50,13 @@ import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentSummaryDTO;
 import nl.tudelft.submit.dto.view.labracore.SubmitSubmissionViewDTO;
 import nl.tudelft.submit.dto.view.script.ScriptTrainResultViewDTO;
 import nl.tudelft.submit.enums.SubmissionDownloadPreference;
+import nl.tudelft.submit.model.AbstractSubmitSubmission;
+import nl.tudelft.submit.model.CoreSubmission;
 import nl.tudelft.submit.model.SubmitAssignment;
-import nl.tudelft.submit.model.SubmitSubmission;
-import nl.tudelft.submit.repository.SubmitSubmissionRepository;
+import nl.tudelft.submit.model.TestSubmission;
+import nl.tudelft.submit.repository.AbstractSubmitSubmissionRepository;
+import nl.tudelft.submit.repository.CoreSubmissionRepository;
+import nl.tudelft.submit.repository.TestSubmissionRepository;
 import nl.tudelft.submit.repository.note.SubmissionNoteRepository;
 
 @Service
@@ -77,9 +81,13 @@ public class SubmissionService {
 	private SubmissionControllerApi submissionApi;
 
 	@Autowired
-	private SubmitSubmissionRepository submissionRepository;
+	private CoreSubmissionRepository submissionRepository;
+	@Autowired
+	private TestSubmissionRepository testSubmissionRepository;
 	@Autowired
 	private SubmissionNoteRepository submissionNoteRepository;
+	@Autowired
+	private AbstractSubmitSubmissionRepository<AbstractSubmitSubmission> abstractSubmitSubmissionRepository;
 
 	@Autowired
 	@Lazy
@@ -123,7 +131,7 @@ public class SubmissionService {
 	@Transactional
 	public Long addSubmission(SubmissionCreateDTO createDTO) {
 		Long id = submissionApi.addSubmission(createDTO).block();
-		submissionRepository.saveAndFlush(SubmitSubmission.builder().id(id).build());
+		submissionRepository.saveAndFlush(CoreSubmission.builder().coreSubmissionId(id).build());
 		return id;
 	}
 
@@ -405,7 +413,8 @@ public class SubmissionService {
 		String downloadsDirectory = tempStorageDir + "/downloads";
 		String submissionsDirectory = storageDir + "/submissions";
 		if (submissionIds.size() == 1) {
-			return fileService.downloadFiles(submissionIds.get(0), submissionsDirectory, downloadsDirectory);
+			return fileService.downloadFiles(getSubmissionIdByCoreId(submissionIds.get(0)),
+					submissionsDirectory, downloadsDirectory);
 		}
 		List<SubmissionDetailsDTO> submissions = submissionCache.get(submissionIds);
 		File customDownloadDir = new File(downloadsDirectory + File.separator + "custom_download");
@@ -444,15 +453,16 @@ public class SubmissionService {
 	}
 
 	private void downloadSubmission(String downloadPath, SubmissionDetailsDTO submission) throws IOException {
+		Long submitSubmissionId = getSubmissionIdByCoreId(submission.getId());
 		String submissionsDirectory = storageDir + "/submissions";
-		Path path = Paths.get(downloadPath + File.separator + "submission_" + submission.getId());
+		Path path = Paths.get(downloadPath + File.separator + "submission_" + submitSubmissionId);
 		Files.createDirectory(path);
 		File submissionDir = new File(submissionsDirectory + File.separator
-				+ StringUtils.cleanPath(fileService.getDirectory(submission.getId())));
+				+ StringUtils.cleanPath(fileService.getDirectory(submitSubmissionId)));
 		File[] files = submissionDir.listFiles();
 		for (File file : files) {
 			Files.copy(file.toPath(), Paths.get(downloadPath + File.separator + "submission_"
-					+ submission.getId() + File.separator + file.getName()));
+					+ submitSubmissionId + File.separator + file.getName()));
 		}
 	}
 
@@ -476,6 +486,7 @@ public class SubmissionService {
 									.getOrCreateSubmitAssignment(a.getId());
 							assignment.setScoreType(findAssignment.getScoreType());
 							assignment.setGuidelines(findAssignment.getGuidelines());
+							assignment.setHidden(findAssignment.getHidden());
 							return assignment;
 						}, a -> getSubmitSubmissions(a.getId(), groupId))));
 		return assignments;
@@ -498,10 +509,10 @@ public class SubmissionService {
 					view.setIsLatest(false);
 					view.setFeedback(feedbackService.getAllFeedbackPerSubmission(s.getId()));
 					view.setNote(submissionNoteRepository.getByEntityId(new SubmissionId(view.getId())));
-					SubmitSubmission submitSubmission = getOrCreateSubmitSubmission(s.getId());
-					view.setScriptResults(View.convert(submitSubmission.getScriptResults(),
+					CoreSubmission coreSubmission = getOrCreateSubmitCoreSubmission(s.getId());
+					view.setScriptResults(View.convert(coreSubmission.getScriptResults(),
 							ScriptTrainResultViewDTO.class));
-					view.setScriptPending(submitSubmission.getScriptPending());
+					view.setScriptPending(coreSubmission.getScriptPending());
 					return view;
 				}).collect(Collectors.toList());
 		if (!submissions.isEmpty()) {
@@ -511,15 +522,16 @@ public class SubmissionService {
 	}
 
 	/**
-	 * Gets the submit submission with the corresponding id or creates one.
+	 * Gets the submit core submission with the corresponding id or creates one.
 	 *
 	 * @param  submissionId The id of the submission
-	 * @return              The submit submission
+	 * @return              The submit core submission
 	 */
 	@Transactional
-	public SubmitSubmission getOrCreateSubmitSubmission(Long submissionId) {
-		return submissionRepository.findById(submissionId).orElseGet(
-				() -> submissionRepository.save(SubmitSubmission.builder().id(submissionId).build()));
+	public CoreSubmission getOrCreateSubmitCoreSubmission(Long submissionId) {
+		return submissionRepository.findByCoreSubmissionId(submissionId).orElseGet(
+				() -> submissionRepository
+						.save(CoreSubmission.builder().coreSubmissionId(submissionId).build()));
 	}
 
 	/**
@@ -530,10 +542,60 @@ public class SubmissionService {
 	 */
 	@Transactional
 	public void setPendingState(Long submissionId, boolean pending) {
-		submissionRepository.findById(submissionId).ifPresent(s -> {
+		abstractSubmitSubmissionRepository.findById(submissionId).ifPresent(s -> {
 			s.setScriptPending(pending);
-			submissionRepository.save(s);
+			abstractSubmitSubmissionRepository.save(s);
 		});
 	}
 
+	/**
+	 * Get SubmitSubmission by id
+	 *
+	 * @param  id The id of the submission
+	 * @return    The SubmitSubmission
+	 */
+	public AbstractSubmitSubmission getSubmitSubmissionById(Long id) {
+		return abstractSubmitSubmissionRepository.findById(id).get();
+	}
+
+	/**
+	 * Get SubmitSubmissionId by core submissionId
+	 *
+	 * @param  coreSubmissionId The core id of the submission
+	 * @return                  the SubmitSubmissionId
+	 */
+	public Long getSubmissionIdByCoreId(Long coreSubmissionId) {
+		return submissionRepository.findByCoreSubmissionId(coreSubmissionId).get().getId();
+	}
+
+	/**
+	 * Get core submission by SubmitSubmissionId
+	 *
+	 * @param  submissionId the id of the submission
+	 * @return              the core submission (if any)
+	 */
+	public Optional<CoreSubmission> getCoreSubBySubmitSubId(Long submissionId) {
+		return submissionRepository.findById(submissionId);
+	}
+
+	/**
+	 * Creates a new test submission for an assignment
+	 *
+	 * @param  assignmentId the assignment id
+	 * @return              the created test submission
+	 */
+	public TestSubmission createTestSubmissionForAssignment(Long assignmentId) {
+		return testSubmissionRepository.save(TestSubmission.builder().assignmentId(assignmentId).build());
+	}
+
+	/**
+	 * Returns the latest test submission for an assignment
+	 *
+	 * @param  assignmentId the assignment id
+	 * @return              the latest test submission (if any)
+	 */
+	public Optional<TestSubmission> getLatestTestSubmissionByAssignment(Long assignmentId) {
+		return testSubmissionRepository.findAllByAssignmentId(assignmentId).stream()
+				.max(java.util.Comparator.comparing(TestSubmission::getCreatedDate));
+	}
 }
diff --git a/src/main/java/nl/tudelft/submit/startup/PublishAllAssignmentsService.java b/src/main/java/nl/tudelft/submit/startup/PublishAllAssignmentsService.java
new file mode 100644
index 0000000000000000000000000000000000000000..7896ca8c7dbd29ea7b6f788b3d6353d9643fc534
--- /dev/null
+++ b/src/main/java/nl/tudelft/submit/startup/PublishAllAssignmentsService.java
@@ -0,0 +1,46 @@
+/*
+ * Submit
+ * Copyright (C) 2020 - Delft University of Technology
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.submit.startup;
+
+import javax.transaction.Transactional;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Service;
+
+import lombok.AllArgsConstructor;
+import nl.tudelft.submit.repository.SubmitAssignmentRepository;
+
+/**
+ * A service that runs during startup. This startup procedure goes through all assignments and publishes them
+ * in Submit.
+ */
+@Service
+@AllArgsConstructor
+@ConditionalOnExpression("#{'${submit.startup.publish-all-assignments}' == 'true'}")
+public class PublishAllAssignmentsService {
+
+	private final SubmitAssignmentRepository submitAssignmentRepository;
+
+	@Transactional
+	@EventListener(ApplicationReadyEvent.class)
+	public void publishAllAssignments() {
+		submitAssignmentRepository.findAll().forEach(assignment -> assignment.setHidden(false));
+	}
+}
diff --git a/src/main/resources/application.template.yaml b/src/main/resources/application.template.yaml
index 4a0d91e0dd77c36a9fbe0c9f38cbdac3f7eb159b..e18e6b1c927613155de43c22a1dcfdc1e584c663 100644
--- a/src/main/resources/application.template.yaml
+++ b/src/main/resources/application.template.yaml
@@ -34,6 +34,8 @@ submit:
     timeout: 1800 # in seconds
   cache:
     person-timeout: 60 # in seconds
+  startup:
+    publish-all-assignments: false
 
 spring:
   profiles:
diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties
index fc079dc99b5c6d125777f6fa236a551df62f8d6a..bdf520f8d64edc037421b6c624405cd1ab7e35b6 100644
--- a/src/main/resources/messages.properties
+++ b/src/main/resources/messages.properties
@@ -140,6 +140,7 @@ module.no_group = No group
 module.progress = Progress
 module.student_search = Username / Group / Student number / Student name
 module.create_groups = Create groups
+module.create_groups.import = Import groups
 module.create_groups.allocate = Allocate
 module.create_groups.generate = Generate
 module.create_groups.amount = Amount of groups
@@ -147,6 +148,11 @@ module.create_groups.amount.enter = Enter amount
 module.export_groups = Export groups
 
 assignment.add = Add assignment
+assignment.show = Show assignment
+assignment.hide = Hide assignment
+assignment.hide.info = This assignment is hidden. It cannot be seen by students. If this assignment should not be hidden, you can make it visible by pressing 'Show assignment' below.
+assignment.draft = Make draft
+assignment.draft.help = By making an assignment draft, only instructors and head TAs will be able to see it.
 assignment.download_file = Download assignment file
 assignment.file.help = Upload any assignment file e.g. assignment description. You can delete it later using the trash button. You can use a .zip file to upload multiple files.
 assignment.file.deleted = File deleted
@@ -154,6 +160,7 @@ assignment.edit = Edit assignment
 assignment.submission = Submission
 assignment.submissions = Submissions
 assignment.submit = Submit
+assignment.test_submission = Test submission
 assignment.no_submissions = No submissions
 assignment.no_submission = No submission
 assignment.versions = Versions
diff --git a/src/main/resources/migrations.yaml b/src/main/resources/migrations.yaml
index 36192c0846c16ffa1e9d35734f53ec0c087f529f..80014e8cad3868cea4a529365d30d1cd3b6f5e92 100644
--- a/src/main/resources/migrations.yaml
+++ b/src/main/resources/migrations.yaml
@@ -538,4 +538,212 @@ databaseChangeLog:
               - column:
                   name: guidelines
                   type: CLOB(32768)
-            tableName: submit_assignment
\ No newline at end of file
+            tableName: submit_assignment
+  - changeSet:
+      id: 1701359245733-1
+      author: dsav (generated)
+      changes:
+        - modifyDataType:
+            columnName: formula
+            newDataType: CLOB(327682)
+            tableName: edition_grading_formula
+  - changeSet:
+      id: 1701359245733-2
+      author: dsav (generated)
+      changes:
+        - modifyDataType:
+            columnName: formula
+            newDataType: CLOB(327683)
+            tableName: module_grading_formula
+  - changeSet:
+      id: 1700042030918-1
+      author: dsav (generated)
+      changes:
+        - addColumn:
+            columns:
+              - column:
+                  constraints:
+                    nullable: false
+                  name: hidden
+                  type: BOOLEAN
+                  defaultValueBoolean: true
+            tableName: submit_assignment
+  - changeSet:
+      id: 1701169247806-2
+      author: dsav (generated)
+      changes:
+        - createTable:
+            columns:
+              - column:
+                  name: core_submission_id
+                  type: BIGINT
+              - column:
+                  constraints:
+                    nullable: false
+                    primaryKey: true
+                    primaryKeyName: CONSTRAINT_B
+                  name: id
+                  type: BIGINT
+            tableName: core_submission
+  - changeSet:
+      id: 1701169247806-1
+      author: dsav (generated)
+      changes:
+        - createTable:
+            columns:
+              - column:
+                  autoIncrement: true
+                  constraints:
+                    nullable: false
+                    primaryKey: true
+                    primaryKeyName: CONSTRAINT_8
+                  name: id
+                  type: BIGINT
+              - column:
+                  constraints:
+                    nullable: false
+                  name: script_pending
+                  type: BIT
+            tableName: abstract_submit_submission
+  - changeSet:
+      id: 1701169247806-3
+      author: dsav (generated)
+      changes:
+        - createTable:
+            columns:
+              - column:
+                  name: assignment_id
+                  type: BIGINT
+              - column:
+                  constraints:
+                    nullable: false
+                  name: created_date
+                  type: TIMESTAMP
+              - column:
+                  constraints:
+                    nullable: false
+                    primaryKey: true
+                    primaryKeyName: CONSTRAINT_3A
+                  name: id
+                  type: BIGINT
+            tableName: test_submission
+  - changeSet:
+      id: 1701169247806-4
+      author: dsav (generated)
+      changes:
+        - addForeignKeyConstraint:
+            baseColumnNames: id
+            baseTableName: test_submission
+            constraintName: FK1awpk7v5us32q8e1u0s4ph46f
+            deferrable: false
+            initiallyDeferred: false
+            onDelete: RESTRICT
+            onUpdate: RESTRICT
+            referencedColumnNames: id
+            referencedTableName: abstract_submit_submission
+            validate: true
+  - changeSet:
+      id: 1701169247806-5
+      author: dsav (generated)
+      changes:
+        - addForeignKeyConstraint:
+            baseColumnNames: submission_id
+            baseTableName: script_result
+            constraintName: FK34g7ed9l54iae3ef1w7vub3f
+            deferrable: false
+            initiallyDeferred: false
+            onDelete: RESTRICT
+            onUpdate: RESTRICT
+            referencedColumnNames: id
+            referencedTableName: abstract_submit_submission
+            validate: true
+  - changeSet:
+      id: 1701169247806-6
+      author: dsav (generated)
+      changes:
+        - addForeignKeyConstraint:
+            baseColumnNames: id
+            baseTableName: core_submission
+            constraintName: FK7eui7k6r612cxrl5dsf84ovjd
+            deferrable: false
+            initiallyDeferred: false
+            onDelete: RESTRICT
+            onUpdate: RESTRICT
+            referencedColumnNames: id
+            referencedTableName: abstract_submit_submission
+            validate: true
+  - changeSet:
+      id: 1701169247806-7
+      author: dsav (generated)
+      changes:
+        - addForeignKeyConstraint:
+            baseColumnNames: submission_id
+            baseTableName: script_wagon_result
+            constraintName: FKcdrsre034hq6mamnf9frthdu9
+            deferrable: false
+            initiallyDeferred: false
+            onDelete: RESTRICT
+            onUpdate: RESTRICT
+            referencedColumnNames: id
+            referencedTableName: abstract_submit_submission
+            validate: true
+  - changeSet:
+      id: 1701169247806-8
+      author: dsav (generated)
+      changes:
+        - addForeignKeyConstraint:
+            baseColumnNames: submission_id
+            baseTableName: script_train_result
+            constraintName: FKkgdkislnb6718bjhkx7nokoy7
+            deferrable: false
+            initiallyDeferred: false
+            onDelete: RESTRICT
+            onUpdate: RESTRICT
+            referencedColumnNames: id
+            referencedTableName: abstract_submit_submission
+            validate: true
+  - changeSet:
+      id: 1701169247806-9
+      author: dsav (generated)
+      changes:
+        - dropForeignKeyConstraint:
+            baseTableName: script_train_result
+            constraintName: FKc3qhjvo3u0bnidlhtjc7xw3f8
+  - changeSet:
+      id: 1701169247806-10
+      author: dsav (generated)
+      changes:
+        - dropForeignKeyConstraint:
+            baseTableName: script_result
+            constraintName: FKtatgidyimgtoja7t71v22cuny
+  - changeSet:
+      id: 1701169247806-11
+      author: dsav (generated)
+      changes:
+        - dropForeignKeyConstraint:
+            baseTableName: script_wagon_result
+            constraintName: FKtj6vpvtpy35jrp4183d9btl7i
+  - changeSet:
+      id: 1701169247806-12
+      author: dsav
+      changes:
+        - sql:
+            comment: Move data to 'abstract_submit_submission'.
+            sql: insert into abstract_submit_submission (script_pending)
+              select s.script_pending from submit_submission as s;
+  - changeSet:
+      id: 1701169247806-13
+      author: dsav
+      changes:
+        - sql:
+            comment: Move data to 'core_submission'.
+            sql: insert into core_submission (core_submission_id, id)
+              select s.id, s.id from submit_submission as s;
+  - changeSet:
+      id: 1701169247806-14
+      author: dsav
+      changes:
+        - sql:
+            comment: Delete and drop submit_submission table.
+            sql: delete from submit_submission;
+              drop table submit_submission;
\ No newline at end of file
diff --git a/src/main/resources/templates/assignment/add.html b/src/main/resources/templates/assignment/add.html
index 9100b59fa862435834c943218fc2d08968e92072..e407c5d1522dd9dd2cefe713d6ce4b931e5421eb 100644
--- a/src/main/resources/templates/assignment/add.html
+++ b/src/main/resources/templates/assignment/add.html
@@ -139,6 +139,18 @@
                             type="checkbox" />
                         <label for="assignment-accept-late" th:text="#{general.allow}"></label>
                     </div>
+                    <div>
+                        <label for="make-draft" th:text="#{assignment.draft}"></label>
+                        <div class="tooltip">
+                            <span class="tooltip__control fa-solid fa-question"></span>
+                            <div role="tooltip" style="white-space: initial; min-width: 20rem">
+                                <p th:text="#{assignment.draft.help}"></p>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="checkbox">
+                        <input id="make-draft" type="checkbox" value="true" th:name="hidden" />
+                    </div>
 
                     <div class="underlined" style="grid-column: span 2"></div>
 
diff --git a/src/main/resources/templates/assignment/test_submission.html b/src/main/resources/templates/assignment/test_submission.html
new file mode 100644
index 0000000000000000000000000000000000000000..19312a8e285b408b17e9a2bebafbc4f52b8b62f3
--- /dev/null
+++ b/src/main/resources/templates/assignment/test_submission.html
@@ -0,0 +1,140 @@
+<!--
+
+    Submit
+    Copyright (C) 2020 - Delft University of Technology
+
+    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 <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <body>
+        <dialog
+            th:fragment="overlay"
+            th:if="${@authorizationService.canSubmitTestSubmission(assignment.id)}"
+            th:id="|test-submission-${assignment.id}-overlay|"
+            class="dialog">
+            <form
+                th:action="@{|/submission/test-submission/${assignment.id}|}"
+                method="post"
+                class="flex vertical p-7"
+                enctype="multipart/form-data">
+                <h1 class="underlined font-500" th:text="#{assignment.submit}"></h1>
+
+                <div class="grid col-2 align-center gap-3" style="--col-1: minmax(0, 8rem)">
+                    <input th:name="assignment.id" th:value="${assignment.id}" type="hidden" />
+
+                    <span
+                        th:unless="${versions.isEmpty() or versions[0].name == '_unit'}"
+                        th:text="#{assignment.versions}"></span>
+                    <div
+                        th:unless="${versions.isEmpty() or versions[0].name == '_unit'}"
+                        class="form-hstack-grid">
+                        <div class="checkbox" th:each="version: ${versions}">
+                            <input
+                                th:id="|version-group-${version.id}|"
+                                th:value="${version.id}"
+                                th:name="versionId"
+                                type="radio" />
+                            <label
+                                th:for="|version-group-${version.id}|"
+                                th:text="${version.name}"></label>
+                        </div>
+                    </div>
+
+                    <label for="file" th:text="#{general.file}"></label>
+                    <input
+                        type="file"
+                        id="file"
+                        name="file"
+                        th:accept="${assignment.allowedFileTypes == null} ? '*' : ${#strings.substring(assignment.allowedFileTypes.toString(), 1, #strings.length(assignment.allowedFileTypes.toString())-1)}"
+                        onchange="readColumnOptions()"
+                        required />
+                </div>
+
+                <div class="flex space-between">
+                    <button
+                        type="button"
+                        class="button p-less"
+                        data-style="outlined"
+                        th:text="#{general.cancel}"
+                        data-cancel></button>
+                    <button
+                        type="submit"
+                        class="button p-less"
+                        th:text="#{assignment.submit}"></button>
+                </div>
+            </form>
+            <div
+                th:id="|script-feedback-${testSubmission.id}|"
+                th:unless="${testSubmission == null || testScriptResults.isEmpty()}"
+                th:with="trainResult = ${testScriptResults[0]}">
+                <div>
+                    <th:block th:each="wagonResult, iter : ${trainResult.subresults}">
+                        <div class="accordion__header flex space-between pil-5 pbl-3">
+                            <div class="flex gap-3 align-center">
+                                <span
+                                    th:text="|${wagonResult.wagon.name} (#{|script.status.${wagonResult.status.name().toLowerCase()}|})|"></span>
+                                <span
+                                    class="chip"
+                                    th:if="${wagonResult.status.name() == 'DONE' and wagonResult.score != null}"
+                                    th:text="${@gradeService.getScoreDisplayString(wagonResult.score, wagonResult.wagon.gradeScheme)}"></span>
+                            </div>
+                            <button
+                                th:aria-controls="|testwagon-${wagonResult.wagon.id}|"
+                                hidden></button>
+                        </div>
+
+                        <div
+                            class="flex gap-3 vertical mb-3"
+                            th:id="|testwagon-${wagonResult.wagon.id}|">
+                            <div th:each="scriptResult : ${wagonResult.subresults}" class="pil-5">
+                                <div class="flex gap-3 align-center">
+                                    <span
+                                        th:text="|${scriptResult.script.name} (#{|script.status.${scriptResult.status.name().toLowerCase()}|})|"></span>
+                                    <span
+                                        class="chip"
+                                        th:if="${scriptResult.status.name() == 'DONE' and scriptResult.score != null}"
+                                        th:text="${@gradeService.getScoreDisplayString(scriptResult.score, scriptResult.script.gradeScheme)}"></span>
+                                </div>
+
+                                <div
+                                    th:if="${scriptResult.feedback.isEmpty()}"
+                                    class="font-200 mt-2">
+                                    <p th:if="${scriptResult.status.isFinished()}">No feedback</p>
+                                    <p th:unless="${scriptResult.status.isFinished()}">
+                                        No feedback yet
+                                    </p>
+                                </div>
+                                <th:block
+                                    th:each="feedback : ${scriptResult.feedback}"
+                                    th:if="${@authorizationService.canSubmitTestSubmission(assignment.id)}">
+                                    <div class="font-200 mt-2">
+                                        <p
+                                            style="max-width: 100ch"
+                                            th:each="text : ${#strings.listSplit(feedback.feedback, '&#10;')}"
+                                            th:text="${text}"></p>
+                                    </div>
+                                </th:block>
+                            </div>
+                        </div>
+                    </th:block>
+                </div>
+            </div>
+        </dialog>
+    </body>
+</html>
diff --git a/src/main/resources/templates/assignment/top.html b/src/main/resources/templates/assignment/top.html
index faec249e4726efb2ea54004ce69941159094ad9e..80f4669ad1c575c886dcc99569372bb5bf4c6188 100644
--- a/src/main/resources/templates/assignment/top.html
+++ b/src/main/resources/templates/assignment/top.html
@@ -48,6 +48,9 @@
             </div>
 
             <h1 class="font-800 mb-3" th:text="${assignment.name}"></h1>
+            <div th:if="${assignment.hidden}" class="banner mb-3" data-type="warning">
+                <p th:text="#{assignment.hide.info}"></p>
+            </div>
 
             <div class="flex gap-3 wrap mb-5">
                 <button
@@ -73,12 +76,29 @@
                     data-style="outlined"
                     th:text="#{grading.import}"
                     data-dialog="import-grades-overlay"></button>
+                <form
+                    th:if="${@authorizationService.canEditAssignment(assignment.id)}"
+                    th:id="toggle-hidden"
+                    method="post"
+                    th:action="@{|/assignment/${assignment.id}/change-visibility|}"
+                    hidden></form>
+                <button
+                    th:if="${@authorizationService.canEditAssignment(assignment.id)}"
+                    class="button"
+                    data-style="outlined"
+                    th:form="toggle-hidden"
+                    th:text="${assignment.hidden} ? #{assignment.show} : #{assignment.hide}"></button>
                 <button
                     th:if="${@authorizationService.canCreateVersion(assignment.id) and versions.isEmpty()}"
                     class="button"
                     data-style="outlined"
                     th:text="#{script.train.add}"
                     data-dialog="add-train-overlay"></button>
+                <button
+                    th:if="${@authorizationService.canSubmitTestSubmission(assignment.id) and not versions.isEmpty()}"
+                    th:data-dialog="|test-submission-${assignment.id}-overlay|"
+                    class="button"
+                    th:text="#{assignment.test_submission}"></button>
             </div>
 
             <div class="tabs mb-5" role="tablist">
@@ -108,6 +128,7 @@
             <div th:replace="~{assignment/import_grades :: overlay}"></div>
             <div th:replace="~{assignment/edit :: overlay}"></div>
             <div th:replace="~{assignment/submit :: overlay('assignment')}"></div>
+            <div th:replace="~{assignment/test_submission :: overlay}"></div>
             <div th:replace="~{assignment/add_train :: overlay}"></div>
         </div>
     </body>
diff --git a/src/main/resources/templates/edition/assist.html b/src/main/resources/templates/edition/assist.html
index 9a38a6fca27cf74d22695787f1f95b882d0b1379..5f5a2d6502aabd71579f900364519a1f43762836 100644
--- a/src/main/resources/templates/edition/assist.html
+++ b/src/main/resources/templates/edition/assist.html
@@ -58,6 +58,7 @@
                         name="q"
                         placeholder="Search for group name, netID, or student name..."
                         th:value="${param.q}" />
+                    <input type="hidden" name="module" th:value="${module?.id}" />
                     <button type="submit" class="fa-solid fa-search"></button>
                 </form>
 
@@ -96,24 +97,26 @@
                             </div>
                         </th:block>
                         <script>
+                            function fetchGroup(group) {
+                                const toReplace =
+                                    group.nextElementSibling.querySelector("[data-fetch]");
+                                if (toReplace == null) return;
+                                fetch(`/group/${toReplace.dataset.fetch}/submissions`)
+                                    .then(r => r.text())
+                                    .then(elem => {
+                                        toReplace.parentElement.innerHTML = elem;
+                                        initComponentEvents(
+                                            document.getElementById(
+                                                `group-${toReplace.dataset.fetch}-submissions`
+                                            )
+                                        );
+                                    })
+                                    .catch(error => {
+                                        console.error("Fetch error:", error);
+                                        toReplace.parentElement.innerHTML = `An error occurred when fetching the data`;
+                                    });
+                            }
                             document.addEventListener("DOMContentLoaded", function () {
-                                function fetchGroup(group) {
-                                    const toReplace =
-                                        group.nextElementSibling.querySelector("[data-fetch]");
-                                    if (toReplace == null) return;
-                                    fetch(`/group/${toReplace.dataset.fetch}/submissions`)
-                                        .then(r => r.text())
-                                        .then(elem => {
-                                            toReplace.parentElement.innerHTML = elem;
-                                            initComponentEvents(
-                                                document.getElementById(
-                                                    `group-${toReplace.dataset.fetch}-submissions`
-                                                )
-                                            );
-                                        })
-                                        .catch(() => window.location.reload());
-                                }
-
                                 const groups = document.getElementById("groups");
                                 groups.querySelectorAll(".accordion__header").forEach(g =>
                                     g.addEventListener("click", function (event) {
@@ -140,13 +143,10 @@
             role="list"
             th:id="|group-${group.id}-submissions|">
             <li
-                th:each="entry : ${group.submissionMap}"
-                th:with="assignment = ${entry.key}, submission = ${entry.value.isEmpty() ? null : entry.value[0]}"
+                th:each="key : ${@assignmentService.getVisibleAssignments(group.submissionMap.keySet())}"
+                th:with="assignment = ${key}, entryValue =${group.submissionMap.get(key)}, submission = ${entryValue.isEmpty() ? null : entryValue[0]}"
                 class="pil-5 mb-3 flex space-between">
                 <div class="flex align-center gap-3">
-                    <span
-                        th:unless="${@authorizationService.canViewAssignment(assignment.id)}"
-                        th:text="${assignment.name}"></span>
                     <a
                         th:if="${@authorizationService.canViewAssignment(assignment.id)}"
                         class="link"
diff --git a/src/main/resources/templates/edition/student.html b/src/main/resources/templates/edition/student.html
index 97dc36b7fd64be2732bf848f799e00d657844900..5777b3db82d9ecc946dd4d6f7d29a0c3e3f0fa3f 100644
--- a/src/main/resources/templates/edition/student.html
+++ b/src/main/resources/templates/edition/student.html
@@ -139,10 +139,12 @@
                         </ul>
                     </div>
 
-                    <div class="accordion" th:unless="${group == null}">
+                    <div
+                        class="accordion"
+                        th:unless="${group == null && !@assignmentService.getVisibleAssignments(submissionMap.entrySet()).isEmpty()}">
                         <th:block
-                            th:each="entry : ${submissionMap}"
-                            th:with="assignment = ${entry.key}, submissions = ${entry.value}">
+                            th:each="key : ${@assignmentService.getVisibleAssignments(submissionMap.keySet())}"
+                            th:with="assignment = ${key}, submissions = ${submissionMap.get(assignment)}">
                             <div class="accordion__header">
                                 <div class="flex space-between pil-5 pbl-3">
                                     <div class="flex align-center gap-3">
diff --git a/src/main/resources/templates/edition/view.html b/src/main/resources/templates/edition/view.html
index 44704367ef625a6de1985f6fdef0f45292aa5950..11eef9e75da5a6b1e1498bdf0b19245004729f91 100644
--- a/src/main/resources/templates/edition/view.html
+++ b/src/main/resources/templates/edition/view.html
@@ -73,15 +73,16 @@
                             </div>
                             <ul
                                 th:id="|module-${module.id}|"
+                                th:with="visibleAssignments = ${@assignmentService.getVisibleAssignments(module.assignments)}"
                                 class="accordion__content list flex vertical gap-3 mb-3"
                                 role="list"
                                 aria-expanded="false">
-                                <li th:if="${module.assignments.isEmpty()}" class="pil-5">
+                                <li th:if="${visibleAssignments.isEmpty()}" class="pil-5">
                                     No assignments
                                 </li>
                                 <li
                                     class="pil-5"
-                                    th:each="assignment : ${#lists.sort(module.assignments, @templateService.nameComparator)}">
+                                    th:each="assignment : ${#lists.sort(visibleAssignments, @templateService.nameComparator)}">
                                     <a
                                         class="link"
                                         th:href="@{/assignment/{id}(id=${assignment.id})}"
diff --git a/src/main/resources/templates/group/view.html b/src/main/resources/templates/group/view.html
index 0fbda7bbe4f909cb14da897a60be5b949e21656c..d5e385bbbdb14cd2089c6a72b15a9af5877bc910 100644
--- a/src/main/resources/templates/group/view.html
+++ b/src/main/resources/templates/group/view.html
@@ -77,6 +77,10 @@
                     th:text="#{group.leave}"></button>
             </div>
 
+            <div class="banner mb-3" data-type="error" th:if="${error != null}">
+                <p th:text="${error}"></p>
+            </div>
+
             <div
                 th:if="${@authorizationService.isStudentInModule(module.id)}"
                 class="grid col-2 mb-5">
@@ -143,20 +147,15 @@
                 <ul id="assignments" class="accordion divided list" role="list">
                     <li
                         class="pbl-3 pil-5"
-                        th:each="entry : ${group.submissionMap.entrySet()}"
-                        th:with="assignment = ${entry.key}, submissions = ${entry.value}, latest = ${submissions.isEmpty() ? null : submissions[0]}, grade = ${latest?.getMaxGrade()}">
+                        th:each="key : ${@assignmentService.getVisibleAssignments(group.submissionMap.keySet())}"
+                        th:with="assignment = ${key}, submissions = ${group.submissionMap.get(key)}, latest = ${submissions.isEmpty() ? null : submissions[0]}, grade = ${latest?.getMaxGrade()}">
                         <div th:data-item="${assignment.id}" class="accordion__header">
                             <div class="flex space-between">
                                 <div>
                                     <a
-                                        th:if="${@authorizationService.canViewAssignment(assignment.id)}"
                                         class="link"
                                         th:href="@{|/assignment/${assignment.id}|}"
                                         th:text="${assignment.name}"></a>
-
-                                    <span
-                                        th:unless="${@authorizationService.canViewAssignment(assignment.id)}"
-                                        th:text="${assignment.name}"></span>
                                     <span
                                         class="chip"
                                         th:if="${grade != null}"
diff --git a/src/main/resources/templates/module/create_groups.html b/src/main/resources/templates/module/create_groups.html
index f559331db34d4d5f85c5c4915abaef5b1fdb0745..0cce7a6feeb25a438563dcd7df0fbec01dc834fa 100644
--- a/src/main/resources/templates/module/create_groups.html
+++ b/src/main/resources/templates/module/create_groups.html
@@ -34,6 +34,7 @@
             <div class="flex gap-3">
                 <button class="button" data-dialog="allocate">Allocate groups</button>
                 <button class="button" data-dialog="generate">Generate groups</button>
+                <button class="button" data-dialog="import">Import groups</button>
             </div>
 
             <dialog id="allocate" class="dialog">
@@ -103,6 +104,55 @@
                     </div>
                 </form>
             </dialog>
+
+            <dialog id="import" class="dialog">
+                <form
+                    class="flex vertical p-7"
+                    th:action="@{|/group/${module.id}/import|}"
+                    method="post"
+                    enctype="multipart/form-data">
+                    <h2 class="underlined font-500" th:text="#{module.create_groups.import}"></h2>
+                    <p>
+                        Note: students must already be part of the edition. You can do so by using
+                        the import feature.
+                    </p>
+                    <div class="grid col-2 align-center gap-3" style="--col-1: minmax(0, 8rem)">
+                        <label for="file" th:text="#{general.file}"></label>
+                        <input
+                            type="file"
+                            id="file"
+                            name="file"
+                            accept="text/csv"
+                            onchange="readColumnOptions()"
+                            required />
+                    </div>
+                    <h3>File format</h3>
+                    <div class="surface">
+                        <div class="flex gap-2">
+                            <div class="grid align-center gap-0">
+                                <span class="font-100">1.</span>
+                                <span class="font-100">2.</span>
+                                <span class="font-100">3.</span>
+                            </div>
+                            <div class="grid gap-0">
+                                <code>Header 1,Header 2</code>
+                                <code>NetID,group name</code>
+                                <code>NetID,group name</code>
+                            </div>
+                        </div>
+                        <em style="font-style: italic">The header row will be ignored</em>
+                    </div>
+                    <div class="flex space-between">
+                        <button class="button p-less" data-style="outlined" data-cancel>
+                            Cancel
+                        </button>
+                        <button
+                            class="button p-less"
+                            type="submit"
+                            th:text="#{module.create_groups.import}"></button>
+                    </div>
+                </form>
+            </dialog>
         </main>
     </body>
 </html>
diff --git a/src/main/resources/templates/module/groups.html b/src/main/resources/templates/module/groups.html
index 1ad00a277db538c07e67f078388b9adacfedc711..2b8cdc8c17fdcdf0a7281e265b991d24bd42f956 100644
--- a/src/main/resources/templates/module/groups.html
+++ b/src/main/resources/templates/module/groups.html
@@ -43,6 +43,9 @@
             <th:block layout:replace="~{module/top :: top}"></th:block>
 
             <div class="flex vertical">
+                <div class="banner mb-3" data-type="error" th:if="${error != null}">
+                    <p th:text="${error}"></p>
+                </div>
                 <form
                     th:if="${@authorizationService.canViewModule(module.id)}"
                     id="group-filter"
diff --git a/src/main/resources/templates/module/view.html b/src/main/resources/templates/module/view.html
index 8a36a0bb77a9235e7f2f064f5786d28b1cf58900..4ca37abb66b7ff993f91ccfa34c41fe86d3876c5 100644
--- a/src/main/resources/templates/module/view.html
+++ b/src/main/resources/templates/module/view.html
@@ -66,15 +66,11 @@
             <ul id="assignments" class="divided list" role="list">
                 <li
                     class="flex space-between pil-5 pbl-3"
-                    th:each="assignment : ${#lists.sort(module.assignments, @templateService.nameComparator)}">
+                    th:each="assignment : ${#lists.sort(@assignmentService.getVisibleAssignments(module.assignments), @templateService.nameComparator)}">
                     <a
-                        th:if="${@authorizationService.canViewAssignment(assignment.id)}"
                         class="link"
                         th:href="@{/assignment/{id}(id=${assignment.id})}"
                         th:text="${assignment.name}"></a>
-                    <span
-                        th:unless="${@authorizationService.canViewAssignment(assignment.id)}"
-                        th:text="${assignment.name}"></span>
                     <a
                         th:if="${@authorizationService.canViewAssignmentSubmissions(assignment.id)}"
                         class="link"
diff --git a/src/main/resources/templates/submission/feedback.html b/src/main/resources/templates/submission/feedback.html
index c06b401c36fe41aa78843c2bf011c35243894287..3a8370f9032fb2ce82184904a30146c858c347db 100644
--- a/src/main/resources/templates/submission/feedback.html
+++ b/src/main/resources/templates/submission/feedback.html
@@ -193,11 +193,10 @@
                                     <th:block
                                         th:each="feedback : ${scriptResult.feedback}"
                                         th:if="${@authorizationService.canViewGroupFeedback(group.id, feedback)}">
-                                        <div class="font-200 mt-2">
+                                        <div class="article mt-2">
                                             <p
                                                 style="max-width: 100ch"
-                                                th:each="text : ${#strings.listSplit(feedback.feedback, '&#10;')}"
-                                                th:text="${text}"></p>
+                                                th:utext="${@markdownService.markdownToHtml(feedback.feedback)}"></p>
                                         </div>
                                     </th:block>
                                 </div>
diff --git a/src/test/java/nl/tudelft/submit/controller/AssignmentControllerTest.java b/src/test/java/nl/tudelft/submit/controller/AssignmentControllerTest.java
index 9c689d7b1abc8200d21a08777bb08edbcc3f637f..8b5f59df73060d6fa809c3bc2d2cd778c20070e2 100644
--- a/src/test/java/nl/tudelft/submit/controller/AssignmentControllerTest.java
+++ b/src/test/java/nl/tudelft/submit/controller/AssignmentControllerTest.java
@@ -49,7 +49,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import application.test.TestSubmitApplication;
 import nl.tudelft.labracore.api.dto.*;
 import nl.tudelft.labracore.lib.api.GradeScheme;
-import nl.tudelft.submit.cache.PersonCacheManager;
 import nl.tudelft.submit.dto.create.SubmitAssignmentCreateDTO;
 import nl.tudelft.submit.dto.create.note.AssignmentNoteCreateDTO;
 import nl.tudelft.submit.dto.helper.VersionDescription;
@@ -62,8 +61,8 @@ import nl.tudelft.submit.dto.view.FeedbackViewDTO;
 import nl.tudelft.submit.dto.view.VersionViewDTO;
 import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentDetailsDTO;
 import nl.tudelft.submit.dto.view.statistics.AssignmentStatisticsDTO;
+import nl.tudelft.submit.model.TestSubmission;
 import nl.tudelft.submit.model.script.ScriptTrain;
-import nl.tudelft.submit.repository.SubmitAssignmentRepository;
 import nl.tudelft.submit.security.AuthorizationService;
 import nl.tudelft.submit.service.*;
 import nl.tudelft.submit.test.TestUserDetailsService;
@@ -87,6 +86,7 @@ class AssignmentControllerTest {
 	private static final SubmitAssignmentCreateDTO ASSIGNMENT_CREATE = new SubmitAssignmentCreateDTO(
 			"Self Study", new Description(), null, null, GradeScheme.DUTCH_GRADE, null, null, null, null,
 			null,
+			null,
 			new ModuleIdDTO().id(MODULE_ID));
 	private static final SubmitAssignmentPatchDTO ASSIGNMENT_PATCH = new SubmitAssignmentPatchDTO();
 	private static final AssignmentModuleDetailsDTO ASSIGNMENT_DETAILS = new AssignmentModuleDetailsDTO()
@@ -99,6 +99,7 @@ class AssignmentControllerTest {
 			.submissions(new ArrayList<>());
 	private static final SubmitAssignmentDetailsDTO SUBMIT_ASSIGNMENT_DETAILS = SubmitAssignmentDetailsDTO
 			.builder()
+			.id(ASSIGNMENT_ID)
 			.name("Chapter 1")
 			.module(new ModuleLayer1DTO()
 					.id(MODULE_ID)
@@ -157,12 +158,6 @@ class AssignmentControllerTest {
 	@MockBean
 	private ScriptService scriptService;
 
-	@Autowired
-	private SubmitAssignmentRepository assignmentRepository;
-
-	@MockBean
-	private PersonCacheManager personCache;
-
 	@MockBean
 	private EditionService editionService;
 
@@ -181,6 +176,7 @@ class AssignmentControllerTest {
 		verify(authService, never()).canEditAssignment(anyLong());
 		verify(assignmentService, never()).getAssignmentDetails(anyLong());
 		verify(versionService, never()).getVersionsPerAssignment(anyLong());
+		verify(submissionService, never()).getLatestTestSubmissionByAssignment(anyLong());
 		verify(studentGroupService, never()).getGroupForPersonInModule(anyLong(), anyLong());
 		verify(versionService, never()).getVersionOfAssignmentForGroup(anyLong(), anyLong());
 	}
@@ -195,6 +191,7 @@ class AssignmentControllerTest {
 		when(assignmentService.isTransparent(anyLong())).thenReturn(false);
 		when(authService.canEditAssignment(anyLong())).thenReturn(true);
 		when(versionService.getVersionsPerAssignment(anyLong())).thenReturn(List.of(VERSION_VIEW));
+		when(submissionService.getLatestTestSubmissionByAssignment(anyLong())).thenReturn(Optional.empty());
 
 		when(assignmentService.getLatestSubmissionsForAllGroups(anyLong())).thenReturn(List.of());
 		when(editionService.getEdition(anyLong()))
@@ -209,9 +206,10 @@ class AssignmentControllerTest {
 				.andExpect(model().attribute("assignment", SUBMIT_ASSIGNMENT_DETAILS))
 				.andExpect(model().attributeExists("versions"));
 
-		verify(authService).canViewAssignment(ASSIGNMENT_ID);
+		verify(authService, atLeastOnce()).canViewAssignment(ASSIGNMENT_ID);
 		verify(assignmentService).getSubmitAssignmentDetails(ASSIGNMENT_ID);
 		verify(versionService).getVersionsPerAssignment(ASSIGNMENT_ID);
+		verify(submissionService).getLatestTestSubmissionByAssignment(anyLong());
 		verify(studentGroupService, never()).getGroupForPersonInModule(anyLong(), anyLong());
 		verify(versionService, never()).getVersionOfAssignmentForGroup(anyLong(), anyLong());
 	}
@@ -228,7 +226,7 @@ class AssignmentControllerTest {
 		when(versionService.getVersionsPerAssignment(anyLong())).thenReturn(List.of(VERSION_VIEW));
 		when(scriptService.getScriptTrainById(anyLong()))
 				.thenReturn(ScriptTrain.builder().wagons(Collections.emptyList()).build());
-
+		when(submissionService.getLatestTestSubmissionByAssignment(anyLong())).thenReturn(Optional.of(TestSubmission.builder().scriptResults(Collections.emptyList()).build()));
 		when(assignmentService.getLatestSubmissionsForAllGroups(anyLong())).thenReturn(List.of());
 		when(editionService.getEdition(anyLong()))
 				.thenReturn(new EditionDetailsDTO().course(new CourseSummaryDTO()));
@@ -241,12 +239,15 @@ class AssignmentControllerTest {
 				.andExpect(model().attributeExists("assignment"))
 				.andExpect(model().attribute("assignment", SUBMIT_ASSIGNMENT_DETAILS))
 				.andExpect(model().attributeExists("train"))
-				.andExpect(model().attributeExists("versions"));
+				.andExpect(model().attributeExists("versions"))
+				.andExpect(model().attributeExists("testSubmission"))
+				.andExpect(model().attributeExists("testScriptResults"));
 
-		verify(authService).canViewAssignment(ASSIGNMENT_ID);
+		verify(authService, atLeastOnce()).canViewAssignment(ASSIGNMENT_ID);
 		verify(assignmentService).getSubmitAssignmentDetails(ASSIGNMENT_ID);
 		verify(versionService).getVersionsPerAssignment(ASSIGNMENT_ID);
 		verify(scriptService).getScriptTrainById(SCRIPT_ID);
+		verify(submissionService).getLatestTestSubmissionByAssignment(anyLong());
 		verify(studentGroupService, never()).getGroupForPersonInModule(anyLong(), anyLong());
 		verify(versionService, never()).getVersionOfAssignmentForGroup(anyLong(), anyLong());
 	}
@@ -278,7 +279,7 @@ class AssignmentControllerTest {
 				.andExpect(model().attributeExists("versions",
 						"submissions", "markSubmissionLate", "canMakeSubmission"));
 
-		verify(authService).canViewAssignment(ASSIGNMENT_ID);
+		verify(authService, atLeastOnce()).canViewAssignment(ASSIGNMENT_ID);
 		verify(assignmentService).getSubmitAssignmentDetails(ASSIGNMENT_ID);
 		verify(versionService).getVersionsPerAssignment(ASSIGNMENT_ID);
 
@@ -353,7 +354,7 @@ class AssignmentControllerTest {
 		mockMvc.perform(get("/assignment/{id}/statistics", ASSIGNMENT_ID))
 				.andExpect(status().isOk());
 
-		verify(authService).canViewAssignmentStatistics(ASSIGNMENT_ID);
+		verify(authService, atLeastOnce()).canViewAssignmentStatistics(ASSIGNMENT_ID);
 		verify(statisticsService).getAssignmentStatistics(any());
 	}
 
@@ -497,11 +498,34 @@ class AssignmentControllerTest {
 				.andExpect(status().isOk())
 				.andExpect(model().attributeExists("assignment", "submissions"));
 
-		verify(authService).canViewAssignmentSubmissions(ASSIGNMENT_ID);
+		verify(authService, atLeastOnce()).canViewAssignmentSubmissions(ASSIGNMENT_ID);
 		verify(assignmentService).getSubmitAssignmentDetails(ASSIGNMENT_ID);
 		verify(assignmentService).getAllSubmissionsFiltered(ASSIGNMENT_ID, "", false);
 	}
 
+	@Test
+	@WithUserDetails("username")
+	void toggleHidePermitted() throws Exception {
+		when(authService.canEditAssignment(anyLong())).thenReturn(true);
+		doNothing().when(assignmentService).toggleHide(anyLong());
+		mockMvc.perform(post("/assignment/{id}/change-visibility", ASSIGNMENT_ID).with(csrf()))
+				.andExpect(status().is3xxRedirection());
+
+		verify(authService).canEditAssignment(ASSIGNMENT_ID);
+		verify(assignmentService).toggleHide(ASSIGNMENT_ID);
+	}
+
+	@Test
+	@WithUserDetails("username")
+	void toggleHideForbidden() throws Exception {
+		when(authService.canEditAssignment(anyLong())).thenReturn(false);
+		mockMvc.perform(post("/assignment/{id}/change-visibility", ASSIGNMENT_ID).with(csrf()))
+				.andExpect(status().isForbidden());
+
+		verify(authService).canEditAssignment(ASSIGNMENT_ID);
+		verify(assignmentService, never()).toggleHide(ASSIGNMENT_ID);
+	}
+
 	@Test
 	@WithUserDetails("username")
 	void importGradesVerifiesCanImportAssignmentGrades() throws Exception {
diff --git a/src/test/java/nl/tudelft/submit/controller/EditionControllerTest.java b/src/test/java/nl/tudelft/submit/controller/EditionControllerTest.java
index c01f94689034b5425fc533e950a8f1071d661f5c..bfdd3f00df07238c8e66b3626592fbd9a8e3fed7 100644
--- a/src/test/java/nl/tudelft/submit/controller/EditionControllerTest.java
+++ b/src/test/java/nl/tudelft/submit/controller/EditionControllerTest.java
@@ -49,7 +49,6 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import application.test.TestSubmitApplication;
 import nl.tudelft.labracore.api.dto.*;
 import nl.tudelft.labracore.lib.api.GradeScheme;
-import nl.tudelft.submit.csv.CSVService;
 import nl.tudelft.submit.dto.create.SubmitEditionCreateDTO;
 import nl.tudelft.submit.dto.create.grading.GradingFormulaCreateDTO;
 import nl.tudelft.submit.dto.helper.GradeExportOptions;
@@ -111,8 +110,6 @@ public class EditionControllerTest {
 	@MockBean
 	private PersonService personService;
 	@MockBean
-	private CSVService csvService;
-	@MockBean
 	private ExportService exportService;
 
 	@MockBean
@@ -124,9 +121,6 @@ public class EditionControllerTest {
 	@MockBean
 	private ApiKeyService apiKeyService;
 
-	@MockBean
-	private FormulaService formulaService;
-
 	@MockBean
 	private StatisticsService statisticsService;
 
diff --git a/src/test/java/nl/tudelft/submit/controller/GradeControllerTest.java b/src/test/java/nl/tudelft/submit/controller/GradeControllerTest.java
index 57a5c068eb3d3b642c25b12e0abc9551d0ae7836..d7be69f82d30597f82c0a2e1a2f3ef81d92bae7e 100644
--- a/src/test/java/nl/tudelft/submit/controller/GradeControllerTest.java
+++ b/src/test/java/nl/tudelft/submit/controller/GradeControllerTest.java
@@ -49,8 +49,8 @@ import nl.tudelft.submit.model.SubmitAssignment;
 import nl.tudelft.submit.model.grading.CalculatedScore;
 import nl.tudelft.submit.security.AuthorizationService;
 import nl.tudelft.submit.service.AssignmentService;
-import nl.tudelft.submit.service.FeedbackService;
 import nl.tudelft.submit.service.GradeService;
+import nl.tudelft.submit.service.SubmissionService;
 
 @AutoConfigureMockMvc
 @SpringBootTest(classes = TestSubmitApplication.class)
@@ -71,9 +71,9 @@ public class GradeControllerTest {
 	@MockBean
 	private GradeService gradeService;
 	@MockBean
-	private FeedbackService feedbackService;
-	@MockBean
 	private AssignmentService assignmentService;
+	@MockBean
+	private SubmissionService submissionService;
 
 	@Test
 	@WithUserDetails("username")
@@ -97,6 +97,7 @@ public class GradeControllerTest {
 		when(authService.canGradeSubmission(anyLong())).thenReturn(true);
 		when(assignmentService.getOrCreateSubmitAssignment(anyLong())).thenReturn(SubmitAssignment.builder()
 				.scoreType(GradeScheme.DUTCH_GRADE).build());
+		when(submissionService.getSubmissionIdByCoreId(anyLong())).thenReturn(SUBMISSION_ID);
 
 		mockMvc.perform(post("/grade/group/{groupId}/{assignmentId}", GROUP_ID, ASSIGNMENT_ID).with(csrf())
 				.flashAttr("feedbackCreate",
@@ -104,6 +105,7 @@ public class GradeControllerTest {
 								.grade(6.0).build()))
 				.andExpect(status().is3xxRedirection());
 
+		verify(submissionService).getSubmissionIdByCoreId(SUBMISSION_ID);
 		verify(authService).canGradeSubmission(SUBMISSION_ID);
 		verify(gradeService).addGradedFeedback(anyLong(), eq(ASSIGNMENT_ID),
 				eq(GradedFeedbackCreateDTO.builder().submissionId(SUBMISSION_ID).textualFeedback("yeet")
diff --git a/src/test/java/nl/tudelft/submit/controller/NotificationControllerTest.java b/src/test/java/nl/tudelft/submit/controller/NotificationControllerTest.java
index a37347cbc3c66cfde8fd711e34ebf6c0b98b9c71..a2495fe70840c5f44ac96da79fa4d26581b4b3f2 100644
--- a/src/test/java/nl/tudelft/submit/controller/NotificationControllerTest.java
+++ b/src/test/java/nl/tudelft/submit/controller/NotificationControllerTest.java
@@ -41,7 +41,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 
 import application.test.TestSubmitApplication;
 import nl.tudelft.submit.dto.view.NotificationViewDTO;
-import nl.tudelft.submit.security.AuthorizationService;
 import nl.tudelft.submit.service.NotificationService;
 
 @AutoConfigureMockMvc
@@ -56,9 +55,6 @@ public class NotificationControllerTest {
 	@MockBean
 	private NotificationService service;
 
-	@MockBean
-	private AuthorizationService authService;
-
 	private static final NotificationViewDTO NOTIFICATION_VIEW = new NotificationViewDTO("Test message",
 			"/test", "username");
 
diff --git a/src/test/java/nl/tudelft/submit/controller/StudentGroupControllerTest.java b/src/test/java/nl/tudelft/submit/controller/StudentGroupControllerTest.java
index d0fe4f3a75409ea0baf71a5025b8af426b73ea89..aa01a4cbedd68911c0e2eab415d2bef46205c534 100644
--- a/src/test/java/nl/tudelft/submit/controller/StudentGroupControllerTest.java
+++ b/src/test/java/nl/tudelft/submit/controller/StudentGroupControllerTest.java
@@ -36,6 +36,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockMultipartFile;
 import org.springframework.security.test.context.support.WithUserDetails;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
@@ -47,6 +48,7 @@ import nl.tudelft.labracore.api.dto.*;
 import nl.tudelft.submit.dto.create.note.GroupNoteCreateDTO;
 import nl.tudelft.submit.dto.id.GroupId;
 import nl.tudelft.submit.dto.view.labracore.SubmitGroupDetailsDTO;
+import nl.tudelft.submit.exception.GroupSwitchingException;
 import nl.tudelft.submit.security.AuthorizationService;
 import nl.tudelft.submit.service.*;
 import nl.tudelft.submit.test.TestUserDetailsService;
@@ -83,15 +85,6 @@ class StudentGroupControllerTest {
 	@MockBean
 	private AssignmentService assignmentService;
 
-	@MockBean
-	private SubmissionService submissionService;
-
-	@MockBean
-	private FeedbackService feedbackService;
-
-	@MockBean
-	private EmailService emailService;
-
 	@MockBean
 	private EditionService editionService;
 
@@ -225,6 +218,24 @@ class StudentGroupControllerTest {
 		verify(groupService).changeCapacities(MODULE_ID, 2);
 	}
 
+	@Test
+	@WithUserDetails("username")
+	void importStudentGroupsTest() throws Exception {
+		when(authService.canImportStudentGroups(anyLong())).thenReturn(true);
+		doNothing().when(groupService)
+				.importStudentGroups(anyLong(), any());
+		MockMultipartFile file = new MockMultipartFile("file", "importfile".getBytes());
+		mockMvc.perform(
+						multipart("/group/{moduleId}/import", MODULE_ID)
+								.file(file).with(csrf())
+								.contentType(MediaType.MULTIPART_FORM_DATA))
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("/module/" + MODULE_ID + "/groups"));;
+
+		verify(authService).canImportStudentGroups(MODULE_ID);
+		verify(groupService).importStudentGroups(MODULE_ID, file);
+	}
+
 	@Test
 	@WithUserDetails("username")
 	void generateGroupsAllowsIfCanCreateGroup() throws Exception {
@@ -271,6 +282,60 @@ class StudentGroupControllerTest {
 		verify(groupService).addPeopleToGroup(GROUP_ID, List.of("username"));
 	}
 
+	@Test
+	@WithUserDetails("username")
+	void joinGroupDoesNotAllowIfAlreadyInAGroup() throws Exception {
+		when(authService.canJoinGroup(anyLong())).thenReturn(true);
+		doThrow(new GroupSwitchingException("Cannot add a person that already has a group")).when(groupService).addPeopleToGroup(any(),anyList());
+		when(moduleService.getModuleById(anyLong()))
+				.thenReturn(new ModuleDetailsDTO().edition(new EditionSummaryDTO().id(1L)).id(MODULE_ID));
+
+		mockMvc.perform(post("/group/{id}/join", GROUP_ID).with(csrf()))
+				.andExpect(flash().attributeExists("error"))
+				.andExpect(redirectedUrl("/module/" + MODULE_ID  + "/groups"));
+
+		verify(authService).canJoinGroup(anyLong());
+		verify(groupService).addPeopleToGroup(GROUP_ID, List.of("username"));
+	}
+
+	@Test
+	@WithUserDetails("username")
+	void createGroupDoesNotAllowIfAlreadyInAGroup() throws Exception {
+		when(authService.canCreateGroup(any())).thenReturn(true);
+		doThrow(new GroupSwitchingException("Cannot create a group with a person that already has a group")).when(groupService).createGroup(any());
+		when(moduleService.getModuleById(anyLong()))
+				.thenReturn(new ModuleDetailsDTO().edition(new EditionSummaryDTO().id(1L)).id(MODULE_ID));
+
+		StudentGroupCreateDTO create =
+				new StudentGroupCreateDTO().module(new ModuleIdDTO().id(MODULE_ID))
+						.members(List.of(new RoleIdDTO().id(new Id().personId(1L))));
+
+		mockMvc.perform(post("/group", GROUP_ID).with(csrf())
+				.contentType(MediaType.APPLICATION_FORM_URLENCODED)
+				.flashAttr("groupCreate", create))
+				.andExpect(flash().attributeExists("error"))
+				.andExpect(redirectedUrl("/module/" + MODULE_ID + "/groups"));
+
+		verify(groupService).createGroup(create);
+	}
+
+	@Test
+	@WithUserDetails("username")
+	void addMembersDoesNotAllowIfAlreadyInGroup() throws Exception {
+		when(authService.canAddMembersToGroup(anyLong())).thenReturn(true);
+		doThrow(new GroupSwitchingException("Cannot add a person that already has a group")).when(groupService).addPeopleToGroup(any(),anyList());
+
+		mockMvc.perform(
+						post("/group/{id}/members", GROUP_ID).with(csrf())
+								.param("usernames", "apond")
+								.param("usernames", "sholmes")
+								.param("usernames", "jsmith"))
+				.andExpect(flash().attributeExists("error"))
+				.andExpect(redirectedUrl("/group/" + GROUP_ID));
+
+		verify(groupService).addPeopleToGroup(GROUP_ID, List.of("apond", "sholmes", "jsmith"));
+	}
+
 	@Test
 	@WithUserDetails("username")
 	void addMembersToGroupVerifiesCanAddMembersToGroup() throws Exception {
diff --git a/src/test/java/nl/tudelft/submit/controller/SubmissionControllerTest.java b/src/test/java/nl/tudelft/submit/controller/SubmissionControllerTest.java
index ca0ebc9445d966826fe545c80bd19dd882440383..a83ddb4bf11eb16e53b52fee83b93d294f8bc315 100644
--- a/src/test/java/nl/tudelft/submit/controller/SubmissionControllerTest.java
+++ b/src/test/java/nl/tudelft/submit/controller/SubmissionControllerTest.java
@@ -58,7 +58,11 @@ import nl.tudelft.submit.dto.create.note.SubmissionNoteCreateDTO;
 import nl.tudelft.submit.dto.id.ScriptTrainIdDTO;
 import nl.tudelft.submit.dto.id.SubmissionId;
 import nl.tudelft.submit.dto.view.VersionViewDTO;
+import nl.tudelft.submit.model.CoreSubmission;
+import nl.tudelft.submit.model.TestSubmission;
 import nl.tudelft.submit.model.UserSettings;
+import nl.tudelft.submit.model.Version;
+import nl.tudelft.submit.model.script.ScriptTrain;
 import nl.tudelft.submit.security.AuthorizationService;
 import nl.tudelft.submit.service.*;
 
@@ -95,6 +99,13 @@ class SubmissionControllerTest {
 			.assignment(new AssignmentIdDTO().id(ASSIGNMENT_ID))
 			.group(new StudentGroupIdDTO().id(GROUP_ID));
 
+	private static final CoreSubmission coreSubmission = CoreSubmission.builder()
+			.scriptPending(false)
+			.scriptResults(Collections.emptyList())
+			.coreSubmissionId(SUBMISSION_ID)
+			.id(SUBMISSION_ID)
+			.build();
+
 	@Value("${submit.filesys.storage-dir}")
 	private String storageDir;
 	@Value("${submit.filesys.temp-storage-dir}")
@@ -142,12 +153,13 @@ class SubmissionControllerTest {
 		when(submissionService.addSubmission(SUB_CREATE_DTO)).thenReturn(SUBMISSION_ID);
 		when(submissionService.canMakeSubmission(anyLong(), anyLong())).thenReturn(true);
 		when(submissionService.checkSubmission(anySet(), any())).thenReturn(true);
+		when(submissionService.getOrCreateSubmitCoreSubmission(anyLong())).thenReturn(coreSubmission);
 		when(userSettingsService.getOrCreateUserSettings(any())).thenReturn(settings);
 		when(assignmentService.getAssignmentDetails(anyLong())).thenReturn(ASSIGNMENT_DETAILS_DTO);
 		when(groupService.getGroupForPersonInModule(anyLong(), anyLong()))
 				.thenReturn(Optional.ofNullable(new StudentGroupDetailsDTO().id(GROUP_ID)));
 		doNothing().when(fileService).uploadFiles(any(), anyLong(), anyString());
-		doNothing().when(scriptService).runScriptTrain(anyLong(), anyLong());
+		doNothing().when(scriptService).runScriptTrain(anyLong(), any());
 
 		mockMvc.perform(MockMvcRequestBuilders.multipart("/submission/group")
 				.file(new MockMultipartFile("file", "somefile".getBytes())).with(csrf())
@@ -156,11 +168,65 @@ class SubmissionControllerTest {
 				.andExpect(status().is3xxRedirection());
 
 		verify(fileService).uploadFiles(any(), anyLong(), anyString());
+		verify(submissionService).getOrCreateSubmitCoreSubmission(anyLong());
 		verify(authService).canSubmitAssignment(anyLong(), anyLong());
 		verify(submissionService).addSubmission(SUB_CREATE_DTO);
 		verify(emailService).sendEmailToAuthPerson(any(MailMessageCreateDTO.class));
 	}
 
+	@Test
+	@WithUserDetails("username")
+	void addTestSubmissionNoVersionId() throws Exception {
+		TestSubmission testSubmission = TestSubmission.builder().id(3L).assignmentId(ASSIGNMENT_ID).build();
+		when(authService.canSubmitTestSubmission(anyLong())).thenReturn(true);
+		when(assignmentService.getAssignmentDetails(anyLong())).thenReturn(ASSIGNMENT_DETAILS_DTO);
+		when(submissionService.checkSubmission(anySet(), any())).thenReturn(true);
+		when(submissionService.createTestSubmissionForAssignment(anyLong())).thenReturn(testSubmission);
+		when(versionService.getVersionsPerAssignment(anyLong()))
+				.thenReturn(List.of(VersionViewDTO.builder().scriptTrain(new ScriptTrainIdDTO(4L)).build()));
+		doNothing().when(fileService).uploadFiles(any(), anyLong(), anyString());
+		doNothing().when(scriptService).runScriptTrain(anyLong(), any());
+
+		mockMvc.perform(
+				MockMvcRequestBuilders.multipart("/submission/test-submission/{assignmentId}", ASSIGNMENT_ID)
+						.file(new MockMultipartFile("file", "somefile".getBytes())).with(csrf())
+						.contentType(MediaType.MULTIPART_FORM_DATA_VALUE))
+				.andExpect(status().is3xxRedirection());
+
+		verify(fileService).uploadFiles(any(), anyLong(), anyString());
+		verify(authService).canSubmitTestSubmission(anyLong());
+		verify(submissionService).checkSubmission(any(), any());
+		verify(submissionService).createTestSubmissionForAssignment(ASSIGNMENT_ID);
+	}
+
+	@Test
+	@WithUserDetails("username")
+	void addTestSubmissionWithVersionId() throws Exception {
+		TestSubmission testSubmission = TestSubmission.builder().id(3L).assignmentId(ASSIGNMENT_ID).build();
+		when(authService.canSubmitTestSubmission(anyLong())).thenReturn(true);
+		when(assignmentService.getAssignmentDetails(anyLong())).thenReturn(ASSIGNMENT_DETAILS_DTO);
+		when(submissionService.checkSubmission(anySet(), any())).thenReturn(true);
+		when(submissionService.createTestSubmissionForAssignment(anyLong())).thenReturn(testSubmission);
+		when(versionService.getVersion(anyLong()))
+				.thenReturn(Version.builder().scriptTrain(ScriptTrain.builder().id(4L).build()).build());
+		doNothing().when(fileService).uploadFiles(any(), anyLong(), anyString());
+		doNothing().when(scriptService).runScriptTrain(anyLong(), any());
+
+		mockMvc.perform(
+				MockMvcRequestBuilders.multipart("/submission/test-submission/{assignmentId}", ASSIGNMENT_ID)
+						.file(new MockMultipartFile("file", "somefile".getBytes()))
+						.param("versionId", String.valueOf(6L)).with(csrf())
+						.contentType(MediaType.MULTIPART_FORM_DATA_VALUE))
+				.andExpect(status().is3xxRedirection());
+
+		verify(fileService).uploadFiles(any(), anyLong(), anyString());
+		verify(authService).canSubmitTestSubmission(anyLong());
+		verify(submissionService).checkSubmission(any(), any());
+		verify(versionService, never()).getVersionsPerAssignment(anyLong());
+		verify(versionService).getVersion(anyLong());
+		verify(submissionService).createTestSubmissionForAssignment(ASSIGNMENT_ID);
+	}
+
 	@Test
 	@WithUserDetails("username")
 	void addSubmissionDoesNotSendOnError() throws Exception {
@@ -168,13 +234,14 @@ class SubmissionControllerTest {
 
 		when(authService.canSubmitAssignment(anyLong(), anyLong())).thenReturn(true);
 		when(submissionService.addSubmission(SUB_CREATE_DTO)).thenReturn(SUBMISSION_ID);
+		when(submissionService.getOrCreateSubmitCoreSubmission(anyLong())).thenReturn(coreSubmission);
 		when(submissionService.canMakeSubmission(anyLong(), anyLong())).thenReturn(true);
 		when(submissionService.checkSubmission(anySet(), any())).thenReturn(true);
 		when(assignmentService.getAssignmentDetails(anyLong())).thenReturn(ASSIGNMENT_DETAILS_DTO);
 		when(groupService.getGroupForPersonInModule(anyLong(), anyLong()))
 				.thenReturn(Optional.ofNullable(new StudentGroupDetailsDTO().id(GROUP_ID)));
 		doThrow(new IOException()).when(fileService).uploadFiles(any(), anyLong(), anyString());
-		doNothing().when(scriptService).runScriptTrain(anyLong(), anyLong());
+		doNothing().when(scriptService).runScriptTrain(anyLong(), any());
 
 		mockMvc.perform(MockMvcRequestBuilders.multipart("/submission/group")
 				.file(new MockMultipartFile("file", "somefile".getBytes())).with(csrf())
@@ -183,6 +250,7 @@ class SubmissionControllerTest {
 				.andExpect(status().is3xxRedirection());
 
 		verify(fileService).uploadFiles(any(), anyLong(), anyString());
+		verify(submissionService).getOrCreateSubmitCoreSubmission(anyLong());
 		verify(authService).canSubmitAssignment(anyLong(), anyLong());
 		verify(submissionService).addSubmission(SUB_CREATE_DTO);
 		verify(emailService, never()).sendEmailToAuthPerson(any(MailMessageCreateDTO.class));
@@ -214,7 +282,7 @@ class SubmissionControllerTest {
 		when(groupService.getGroupForPersonInModule(anyLong(), anyLong()))
 				.thenReturn(Optional.ofNullable(new StudentGroupDetailsDTO().id(GROUP_ID)));
 		doThrow(new IOException()).when(fileService).uploadFiles(any(), anyLong(), anyString());
-		doNothing().when(scriptService).runScriptTrain(anyLong(), anyLong());
+		doNothing().when(scriptService).runScriptTrain(anyLong(), any());
 
 		mockMvc.perform(MockMvcRequestBuilders.multipart("/submission/group")
 				.file(new MockMultipartFile("file.yeet", "somefile".getBytes())).with(csrf())
@@ -267,6 +335,7 @@ class SubmissionControllerTest {
 	@WithUserDetails("username")
 	void downloadSubmissionAllowsIfCanDownloadSubmission() throws Exception {
 		when(authService.canDownloadSubmission(anyLong())).thenReturn(true);
+		when(submissionService.getSubmissionIdByCoreId(anyLong())).thenReturn(SUBMISSION_ID);
 
 		String submissionPath = "./testfiles";
 		File dir = new File(submissionPath);
@@ -283,6 +352,7 @@ class SubmissionControllerTest {
 				.andExpect(status().isOk());
 
 		verify(authService).canDownloadSubmission(SUBMISSION_ID);
+		verify(submissionService).getSubmissionIdByCoreId(anyLong());
 		verify(fileService).downloadFiles(SUBMISSION_ID, storageDir + "/submissions",
 				tempStorageDir + "/downloads");
 
@@ -298,6 +368,7 @@ class SubmissionControllerTest {
 		mockMvc.perform(get("/submission/{submissionId}/download", SUBMISSION_ID))
 				.andExpect(status().isForbidden());
 
+		verify(submissionService, never()).getSubmissionIdByCoreId(anyLong());
 		verify(authService).canDownloadSubmission(SUBMISSION_ID);
 		verifyNoInteractions(fileService);
 	}
@@ -475,7 +546,7 @@ class SubmissionControllerTest {
 				.andExpect(status().isForbidden());
 
 		verify(authService).canResubmitSubmission(SUBMISSION_ID);
-		verify(scriptService, never()).runScriptTrain(anyLong(), anyLong());
+		verify(scriptService, never()).runScriptTrain(anyLong(), any());
 	}
 
 	@Test
@@ -494,7 +565,7 @@ class SubmissionControllerTest {
 				.andExpect(status().isOk());
 
 		verify(authService).canResubmitSubmission(SUBMISSION_ID);
-		verify(scriptService).runScriptTrain(anyLong(), anyLong());
+		verify(scriptService).runScriptTrain(anyLong(), any());
 	}
 
 	@ParameterizedTest
diff --git a/src/test/java/nl/tudelft/submit/security/AuthorizationServiceTest.java b/src/test/java/nl/tudelft/submit/security/AuthorizationServiceTest.java
index 1396444937f0dc7d982cbbf61c7f478fedc1bcb2..64fa0d582b0d683e2d480c44a05ade40d35577ff 100644
--- a/src/test/java/nl/tudelft/submit/security/AuthorizationServiceTest.java
+++ b/src/test/java/nl/tudelft/submit/security/AuthorizationServiceTest.java
@@ -45,10 +45,12 @@ import nl.tudelft.submit.cache.*;
 import nl.tudelft.submit.dto.id.ScriptTrainIdDTO;
 import nl.tudelft.submit.dto.view.FeedbackViewDTO;
 import nl.tudelft.submit.dto.view.VersionViewDTO;
+import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentDetailsDTO;
 import nl.tudelft.submit.model.Version;
 import nl.tudelft.submit.model.script.SubmissionKey;
 import nl.tudelft.submit.repository.SubmissionKeyRepository;
 import nl.tudelft.submit.repository.VersionRepository;
+import nl.tudelft.submit.service.AssignmentService;
 import nl.tudelft.submit.service.VersionService;
 import nl.tudelft.submit.test.TestUserDetailsService;
 import reactor.core.publisher.Mono;
@@ -92,6 +94,8 @@ class AuthorizationServiceTest {
 	@MockBean
 	private AssignmentCacheManager assignmentCache;
 	@MockBean
+	private AssignmentService assignmentService;
+	@MockBean
 	private SubmissionCacheManager submissionCache;
 	@MockBean
 	private StudentGroupCacheManager groupCache;
@@ -640,8 +644,22 @@ class AuthorizationServiceTest {
 	@ParameterizedTest
 	@CsvSource({ "TEACHER,true", "HEAD_TA,true", "TA,true", "STUDENT,true", ",false" })
 	@WithUserDetails("username")
-	void canViewAssignment(RoleDetailsDTO.TypeEnum role, boolean expected) {
+	void canViewPublishedAssignment(RoleDetailsDTO.TypeEnum role, boolean expected) {
+		mockRole(role);
+		SubmitAssignmentDetailsDTO assignment = SubmitAssignmentDetailsDTO.builder().id(ASSIGNMENT_ID)
+				.hidden(false).build();
+		when(assignmentService.getSubmitAssignmentDetails(anyLong())).thenReturn(assignment);
+		assertThat(service.canViewAssignment(ASSIGNMENT_ID)).isEqualTo(expected);
+	}
+
+	@ParameterizedTest
+	@CsvSource({ "TEACHER,true", "HEAD_TA,true", "TA,false", "STUDENT,false", ",false" })
+	@WithUserDetails("username")
+	void canViewHiddenAssignment(RoleDetailsDTO.TypeEnum role, boolean expected) {
 		mockRole(role);
+		SubmitAssignmentDetailsDTO assignment = SubmitAssignmentDetailsDTO.builder().id(ASSIGNMENT_ID)
+				.hidden(true).build();
+		when(assignmentService.getSubmitAssignmentDetails(anyLong())).thenReturn(assignment);
 		assertThat(service.canViewAssignment(ASSIGNMENT_ID)).isEqualTo(expected);
 	}
 
@@ -856,6 +874,14 @@ class AuthorizationServiceTest {
 		assertThat(service.canCreateGroup(MODULE_ID)).isEqualTo(expected);
 	}
 
+	@ParameterizedTest
+	@CsvSource({ "TEACHER,true", "HEAD_TA,true", "TA,false", "STUDENT,false", ",false" })
+	@WithUserDetails("username")
+	void canImportStudentGroups(RoleDetailsDTO.TypeEnum role, boolean expected) {
+		mockRole(role);
+		assertThat(service.canImportStudentGroups(MODULE_ID)).isEqualTo(expected);
+	}
+
 	@ParameterizedTest
 	@CsvSource({ "TEACHER,true", "HEAD_TA,true", "TA,true", "STUDENT,false", ",false" })
 	@WithUserDetails("username")
diff --git a/src/test/java/nl/tudelft/submit/service/AssignmentServiceTest.java b/src/test/java/nl/tudelft/submit/service/AssignmentServiceTest.java
index 21f8cfb6aae6c5546a6f24df0a076eed6c3e02a4..3e92a8df2a7872b0747b91707f2a520ab612e9e5 100644
--- a/src/test/java/nl/tudelft/submit/service/AssignmentServiceTest.java
+++ b/src/test/java/nl/tudelft/submit/service/AssignmentServiceTest.java
@@ -21,8 +21,7 @@ import static java.time.LocalDateTime.now;
 import static java.util.Collections.emptyList;
 import static java.util.Collections.emptySet;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.Mockito.*;
 
 import java.io.File;
@@ -31,6 +30,7 @@ import java.io.StringReader;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.time.LocalDateTime;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
 
@@ -59,12 +59,14 @@ import nl.tudelft.submit.dto.patch.SubmitAssignmentPatchDTO;
 import nl.tudelft.submit.dto.patch.grading.AssignmentGradeImportDTO;
 import nl.tudelft.submit.dto.view.VersionViewDTO;
 import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentDetailsDTO;
+import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentSummaryDTO;
 import nl.tudelft.submit.dto.view.labracore.SubmitGroupSummaryDTO;
 import nl.tudelft.submit.dto.view.labracore.SubmitSubmissionViewDTO;
 import nl.tudelft.submit.enums.GradeImportIdType;
+import nl.tudelft.submit.model.CoreSubmission;
 import nl.tudelft.submit.model.SubmitAssignment;
-import nl.tudelft.submit.model.SubmitSubmission;
 import nl.tudelft.submit.repository.SubmitAssignmentRepository;
+import nl.tudelft.submit.security.AuthorizationService;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
@@ -102,6 +104,9 @@ class AssignmentServiceTest {
 	@Autowired
 	private SubmitAssignmentRepository assignmentRepository;
 
+	@MockBean
+	private AuthorizationService authorizationService;
+
 	@Autowired
 	private AssignmentService assignmentService;
 
@@ -317,8 +322,8 @@ class AssignmentServiceTest {
 						.id(SUBMISSION_ID + 2)
 						.group(new StudentGroupSummaryDTO().id(GROUP_ID + 1))
 						.submissionTime(now())));
-		when(submissionService.getOrCreateSubmitSubmission(anyLong()))
-				.thenReturn(SubmitSubmission.builder().build());
+		when(submissionService.getOrCreateSubmitCoreSubmission(anyLong()))
+				.thenReturn(CoreSubmission.builder().build());
 
 		List<SubmitSubmissionViewDTO> result = assignmentService.getAllSubmissions(ASSIGNMENT_ID);
 
@@ -405,6 +410,61 @@ class AssignmentServiceTest {
 		Files.delete(Paths.get(assignmentPath));
 	}
 
+	@Test
+	public void testToggleHide_shouldHideAssignment() {
+		SubmitAssignment assignment = assignmentService.getOrCreateSubmitAssignment(ASSIGNMENT_ID);
+		assignmentService.toggleHide(ASSIGNMENT_ID);
+		assertFalse(assignment.getHidden());
+		assertFalse(assignmentService.getOrCreateSubmitAssignment(ASSIGNMENT_ID).getHidden());
+	}
+
+	@Test
+	void testGetVisibleAssignmentsAllAreVisible() {
+		List<SubmitAssignmentSummaryDTO> allAssignments = Arrays.asList(
+				SubmitAssignmentSummaryDTO.builder().id(1L).build(),
+				SubmitAssignmentSummaryDTO.builder().id(2L).build(),
+				SubmitAssignmentSummaryDTO.builder().id(3L).build());
+		when(authorizationService.canViewAssignment(anyLong())).thenReturn(true);
+
+		List<SubmitAssignmentSummaryDTO> visibleAssignments = assignmentService
+				.getVisibleAssignments(allAssignments);
+
+		assertEquals(3, visibleAssignments.size());
+		assertEquals(visibleAssignments, allAssignments);
+	}
+
+	@Test
+	void testGetVisibleAssignmentsNoneAreVisible() {
+		List<SubmitAssignmentSummaryDTO> allAssignments = Arrays.asList(
+				SubmitAssignmentSummaryDTO.builder().id(1L).build(),
+				SubmitAssignmentSummaryDTO.builder().id(2L).build(),
+				SubmitAssignmentSummaryDTO.builder().id(3L).build());
+		when(authorizationService.canViewAssignment(anyLong())).thenReturn(false);
+
+		List<SubmitAssignmentSummaryDTO> visibleAssignments = assignmentService
+				.getVisibleAssignments(allAssignments);
+
+		assertTrue(visibleAssignments.isEmpty());
+	}
+
+	@Test
+	void testGetVisibleAssignmentsSomeAreVisible() {
+		SubmitAssignmentSummaryDTO visibleAssignment = SubmitAssignmentSummaryDTO.builder().id(2L).build();
+		List<SubmitAssignmentSummaryDTO> allAssignments = Arrays.asList(
+				SubmitAssignmentSummaryDTO.builder().id(1L).build(),
+				visibleAssignment,
+				SubmitAssignmentSummaryDTO.builder().id(3L).build());
+		when(authorizationService.canViewAssignment(1L)).thenReturn(false);
+		when(authorizationService.canViewAssignment(2L)).thenReturn(true);
+		when(authorizationService.canViewAssignment(3L)).thenReturn(false);
+
+		List<SubmitAssignmentSummaryDTO> visibleAssignments = assignmentService
+				.getVisibleAssignments(allAssignments);
+
+		assertEquals(1, visibleAssignments.size());
+		assertEquals(visibleAssignments.get(0), visibleAssignment);
+	}
+
 	@Test
 	void createSubmitAssignment() {
 		assertThat(assignmentRepository.findById(ASSIGNMENT_ID)).isNotPresent();
@@ -421,6 +481,7 @@ class AssignmentServiceTest {
 				.id(ASSIGNMENT_ID)
 				.module(new ModuleLayer1DTO().id(MODULE_ID))
 				.scoreType(GradeScheme.DUTCH_GRADE)
+				.hidden(true)
 				.allowedFileTypes(emptyList())
 				.build();
 		assertThat(assignmentService.getSubmitAssignmentDetails(ASSIGNMENT_ID)).isEqualTo(expected);
diff --git a/src/test/java/nl/tudelft/submit/service/StudentGroupServiceTest.java b/src/test/java/nl/tudelft/submit/service/StudentGroupServiceTest.java
index b354b69d675822934fa830048327b067e75bfe6c..bfae1318eba8111632ce4f5500efe1901eda39a9 100644
--- a/src/test/java/nl/tudelft/submit/service/StudentGroupServiceTest.java
+++ b/src/test/java/nl/tudelft/submit/service/StudentGroupServiceTest.java
@@ -18,10 +18,13 @@
 package nl.tudelft.submit.service;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
 
+import java.io.IOException;
+import java.io.InputStreamReader;
 import java.time.LocalDateTime;
 import java.util.*;
 
@@ -32,12 +35,17 @@ import org.mockito.Mockito;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.mock.web.MockMultipartFile;
+
+import com.opencsv.exceptions.CsvValidationException;
 
 import application.test.TestSubmitApplication;
 import nl.tudelft.labracore.api.StudentGroupControllerApi;
 import nl.tudelft.labracore.api.dto.*;
 import nl.tudelft.librador.dto.view.View;
+import nl.tudelft.submit.cache.PersonCacheManager;
 import nl.tudelft.submit.cache.StudentGroupCacheManager;
+import nl.tudelft.submit.csv.CSVService;
 import nl.tudelft.submit.dto.id.GroupId;
 import nl.tudelft.submit.dto.view.labracore.SubmitGroupDetailsDTO;
 import nl.tudelft.submit.dto.view.labracore.SubmitGroupSummaryDTO;
@@ -45,6 +53,7 @@ import nl.tudelft.submit.enums.SwitchAction;
 import nl.tudelft.submit.exception.GroupSwitchingException;
 import nl.tudelft.submit.model.*;
 import nl.tudelft.submit.model.note.GroupNote;
+import nl.tudelft.submit.repository.SubmitGroupRepository;
 import nl.tudelft.submit.repository.SwitchEventRepository;
 import nl.tudelft.submit.repository.note.GroupNoteRepository;
 import reactor.core.publisher.Flux;
@@ -56,6 +65,7 @@ class StudentGroupServiceTest {
 
 	private final Long GROUP_ID = 5L;
 	private final Long MODULE_ID = 42L;
+	private final Long EDITION_ID = 33L;
 	private final Long COURSE_ID = 139L;
 	private final Long MEMBER_ID = 56123L;
 	private final Long PERSON_ID = 98451L;
@@ -65,6 +75,9 @@ class StudentGroupServiceTest {
 	@MockBean
 	private StudentGroupCacheManager groupCache;
 
+	@MockBean
+	private PersonCacheManager personCache;
+
 	@Autowired
 	private StudentGroupControllerApi groupApi;
 
@@ -80,6 +93,9 @@ class StudentGroupServiceTest {
 	@Autowired
 	private GroupNoteRepository groupNoteRepository;
 
+	@MockBean
+	private SubmitGroupRepository groupRepository;
+
 	@MockBean
 	private ModuleDivisionService moduleDivisionService;
 
@@ -89,6 +105,12 @@ class StudentGroupServiceTest {
 	@MockBean
 	private SubmissionService submissionService;
 
+	@MockBean
+	private CSVService csvService;
+
+	@MockBean
+	private ModuleService moduleService;
+
 	@Test
 	void getGroupDetails() {
 		StudentGroupDetailsDTO dto = new StudentGroupDetailsDTO().id(GROUP_ID).name("Test group")
@@ -203,6 +225,10 @@ class StudentGroupServiceTest {
 		List<String> usernames = List.of("hpotter, rweasley, hgranger");
 		Mockito.when(groupApi.isGroupLocked(anyLong())).thenReturn(Mono.just(false));
 		Mockito.when(groupApi.addGroupMembers(anyLong(), anyList())).thenReturn(Mono.empty());
+		Mockito.when(groupCache.getOrThrow(any())).thenReturn(
+				new StudentGroupDetailsDTO().id(GROUP_ID).module(new ModuleSummaryDTO().id(MODULE_ID)));
+		Mockito.when(personCache.get(anyString())).thenReturn(new PersonSummaryDTO().id(22L));
+		Mockito.when(groupApi.getGroupForPersonAndModule(anyLong(), anyLong())).thenReturn(Mono.empty());
 
 		groupService.addPeopleToGroup(GROUP_ID, usernames);
 
@@ -210,9 +236,49 @@ class StudentGroupServiceTest {
 		Mockito.verify(groupApi).addGroupMembers(GROUP_ID, usernames);
 	}
 
+	@Test
+	public void importStudentGroupsTest() throws IOException, CsvValidationException {
+		MockMultipartFile file = new MockMultipartFile("file", "test.csv", "text/csv", "content".getBytes());
+		List<String[]> lines = List.of(
+				new String[] { "dduck", "Group1" },
+				new String[] { "mmouse", "Group1" },
+				new String[] { "batman", "Group2" });
+		PersonSummaryDTO person1 = new PersonSummaryDTO().id(1L).username("dduck").displayName("Donald Duck");
+		PersonSummaryDTO person2 = new PersonSummaryDTO().id(2L).username("mmouse")
+				.displayName("Mickey Mouse");
+		PersonSummaryDTO person3 = new PersonSummaryDTO().id(3L).username("batman").displayName("Batman");
+
+		when(csvService.readCSV(any(InputStreamReader.class), eq(false), eq(1))).thenReturn(lines);
+		when(moduleService.getModuleById(eq(MODULE_ID))).thenReturn(
+				new ModuleDetailsDTO().id(MODULE_ID).edition(new EditionSummaryDTO().id(EDITION_ID)));
+		when(personCache.get(eq("dduck"))).thenReturn(person1);
+		when(personCache.get(eq("mmouse"))).thenReturn(person2);
+		when(personCache.get(eq("batman"))).thenReturn(person3);
+		when(personCache.get(eq(1L))).thenReturn(Optional.ofNullable(person1));
+		when(personCache.get(eq(2L))).thenReturn(Optional.ofNullable(person2));
+		when(personCache.get(eq(3L))).thenReturn(Optional.ofNullable(person3));
+		when(groupApi.getGroupForPersonAndModule(anyLong(), anyLong())).thenReturn(Mono.empty());
+		when(groupApi.addGroup(any(StudentGroupCreateDTO.class))).thenReturn(Mono.just(5L));
+		when(groupRepository.save(any(SubmitGroup.class))).thenReturn(SubmitGroup.builder().id(5L).build());
+
+		groupService.importStudentGroups(MODULE_ID, file);
+
+		verify(csvService).readCSV(any(InputStreamReader.class), eq(false), eq(1));
+		verify(moduleService, times(3)).getModuleById(eq(MODULE_ID));
+		verify(personCache, times(6)).get(anyString());
+		verify(personCache, times(3)).get(anyLong());
+		verify(groupRepository, times(2)).save(any(SubmitGroup.class));
+	}
+
 	@Test
 	void addPeopleToLockedGroup() {
 		List<String> usernames = List.of("hpotter", "rweasley", "hgranger");
+
+		Mockito.when(groupCache.getOrThrow(any())).thenReturn(
+				new StudentGroupDetailsDTO().id(GROUP_ID).module(new ModuleSummaryDTO().id(MODULE_ID)));
+		Mockito.when(personCache.get(anyString())).thenReturn(new PersonSummaryDTO().id(22L));
+		Mockito.when(groupApi.getGroupForPersonAndModule(anyLong(), anyLong())).thenReturn(Mono.empty());
+
 		Mockito.when(groupApi.isGroupLocked(anyLong())).thenReturn(Mono.just(true));
 		Mockito.when(groupApi.addGroupMembers(anyLong(), anyList())).thenReturn(Mono.empty());
 
@@ -227,16 +293,77 @@ class StudentGroupServiceTest {
 		Mockito.when(groupApi.isGroupLockedAndRoleStudent(anyLong(), anyLong())).thenReturn(Mono.just(false));
 		Mockito.when(groupApi.addMemberToGroup(anyLong(), anyLong())).thenReturn(Mono.empty());
 
+		Mockito.when(groupCache.getOrThrow(any())).thenReturn(
+				new StudentGroupDetailsDTO().id(GROUP_ID).module(new ModuleSummaryDTO().id(MODULE_ID)));
+		Mockito.when(personCache.get(anyString())).thenReturn(new PersonSummaryDTO().id(22L));
+		Mockito.when(groupApi.getGroupForPersonAndModule(anyLong(), anyLong())).thenReturn(Mono.empty());
+
 		assertThat(groupService.addPersonToGroup(MEMBER_ID, GROUP_ID)).isEqualTo(-1L);
 
 		Mockito.verify(groupApi).addMemberToGroup(GROUP_ID, MEMBER_ID);
 		Mockito.verify(groupApi).isGroupLockedAndRoleStudent(MEMBER_ID, GROUP_ID);
 	}
 
+	@Test
+	void cantAddStudentWhenStudentIsAlreadyInAGroup() {
+		StudentGroupDetailsDTO groupDetails = new StudentGroupDetailsDTO().id(77L)
+				.module(new ModuleSummaryDTO().id(MODULE_ID)).name("group name");
+		Mockito.when(groupApi.getGroupForPersonAndModule(anyLong(), anyLong()))
+				.thenReturn(Mono.just(groupDetails));
+		Mockito.when(groupCache.getOrThrow(anyLong())).thenReturn(groupDetails);
+		Mockito.when(personCache.get(anyLong()))
+				.thenReturn(Optional.of(new PersonSummaryDTO().displayName("person name")));
+
+		assertThatThrownBy(() -> groupService.addPersonToGroup(MEMBER_ID, GROUP_ID))
+				.isInstanceOf(GroupSwitchingException.class)
+				.hasMessage("Cannot add person to group because person name is already in group name");
+
+	}
+
+	@Test
+	void cantCreateGroupWhenStudentIsAlreadyInAGroup() {
+		StudentGroupCreateDTO create = new StudentGroupCreateDTO().module(new ModuleIdDTO().id(MODULE_ID))
+				.members(List.of(new RoleIdDTO().id(new Id().personId(MEMBER_ID))));
+		Mockito.when(personCache.get(MEMBER_ID)).thenReturn(Optional
+				.of(new PersonSummaryDTO().username("personname").displayName("person name").id(MEMBER_ID)));
+		Mockito.when(personCache.get("personname")).thenReturn(new PersonSummaryDTO().id(MEMBER_ID));
+		StudentGroupDetailsDTO groupDetails = new StudentGroupDetailsDTO().id(77L).name("group name");
+		Mockito.when(groupApi.getGroupForPersonAndModule(anyLong(), anyLong()))
+				.thenReturn(Mono.just(groupDetails));
+		Mockito.when(groupCache.getOrThrow(anyLong())).thenReturn(groupDetails);
+
+		assertThatThrownBy(() -> groupService.createGroup(create))
+				.isInstanceOf(GroupSwitchingException.class)
+				.hasMessage("Group cannot be created because person name is already in group name");
+
+	}
+
+	@Test
+	void cantAddStudentsWhenOneIsAlreadyInAGroup() {
+		List<String> usernames = List.of("hpotter", "rweasley", "hgranger");
+		StudentGroupDetailsDTO groupDetails = new StudentGroupDetailsDTO().id(77L).name("group name");
+		Mockito.when(groupApi.getGroupForPersonAndModule(anyLong(), anyLong()))
+				.thenReturn(Mono.just(groupDetails));
+		Mockito.when(groupCache.getOrThrow(any())).thenReturn(
+				new StudentGroupDetailsDTO().id(GROUP_ID).module(new ModuleSummaryDTO().id(MODULE_ID)));
+		Mockito.when(personCache.get(anyString())).thenReturn(new PersonSummaryDTO().id(MEMBER_ID));
+		Mockito.when(personCache.get(MEMBER_ID)).thenReturn(Optional
+				.of(new PersonSummaryDTO().id(MEMBER_ID).username("personname").displayName("person name")));
+
+		assertThatThrownBy(() -> groupService.addPeopleToGroup(MEMBER_ID, usernames))
+				.isInstanceOf(GroupSwitchingException.class)
+				.hasMessage("Cannot add person to group because person name is already in group name");
+
+	}
+
 	@Test
 	void addStudentToLockedGroup() {
 		Mockito.when(groupApi.isGroupLockedAndRoleStudent(anyLong(), anyLong())).thenReturn(Mono.just(true));
 		Mockito.when(groupApi.addMemberToGroup(anyLong(), anyLong())).thenReturn(Mono.empty());
+		Mockito.when(groupCache.getOrThrow(any())).thenReturn(
+				new StudentGroupDetailsDTO().id(GROUP_ID).module(new ModuleSummaryDTO().id(MODULE_ID)));
+		Mockito.when(personCache.get(anyString())).thenReturn(new PersonSummaryDTO().id(22L));
+		Mockito.when(groupApi.getGroupForPersonAndModule(anyLong(), anyLong())).thenReturn(Mono.empty());
 
 		Long eventId = groupService.addPersonToGroup(MEMBER_ID, GROUP_ID);
 		SwitchEvent event = switchEventRepository.findByIdOrThrow(eventId);
diff --git a/src/test/java/nl/tudelft/submit/service/SubmissionServiceTest.java b/src/test/java/nl/tudelft/submit/service/SubmissionServiceTest.java
index e5f1bdf0b3656806f59489e9fca114d7481bde3f..d46192201357ccbab5e4d26cf07606d54df8ba39 100644
--- a/src/test/java/nl/tudelft/submit/service/SubmissionServiceTest.java
+++ b/src/test/java/nl/tudelft/submit/service/SubmissionServiceTest.java
@@ -18,6 +18,7 @@
 package nl.tudelft.submit.service;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.Mockito.*;
 
 import java.io.File;
@@ -43,7 +44,12 @@ import nl.tudelft.labracore.api.dto.*;
 import nl.tudelft.submit.cache.SubmissionCacheManager;
 import nl.tudelft.submit.dto.create.SubmissionDownloadConfigCreateDTO;
 import nl.tudelft.submit.enums.SubmissionDownloadPreference;
-import nl.tudelft.submit.repository.SubmitSubmissionRepository;
+import nl.tudelft.submit.model.AbstractSubmitSubmission;
+import nl.tudelft.submit.model.CoreSubmission;
+import nl.tudelft.submit.model.TestSubmission;
+import nl.tudelft.submit.repository.AbstractSubmitSubmissionRepository;
+import nl.tudelft.submit.repository.CoreSubmissionRepository;
+import nl.tudelft.submit.repository.TestSubmissionRepository;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
@@ -79,17 +85,16 @@ class SubmissionServiceTest {
 	@Autowired
 	private SubmissionService submissionService;
 
-	@Autowired
-	private SubmitSubmissionRepository submissionRepository;
-
 	@MockBean
 	private AssignmentService assignmentService;
+	@MockBean
+	private CoreSubmissionRepository submissionRepository;
 
 	@MockBean
-	private StudentGroupService groupService;
+	private TestSubmissionRepository testSubmissionRepository;
 
 	@MockBean
-	private GradeService gradeService;
+	private AbstractSubmitSubmissionRepository<AbstractSubmitSubmission> abstractSubmitSubmissionRepository;
 
 	@MockBean
 	private FileService fileService;
@@ -419,12 +424,54 @@ class SubmissionServiceTest {
 				.isEqualTo(List.of(Objects.requireNonNull(SUB_DETAILED_DTO.getId())));
 	}
 
+	@Test
+	void getSubmitSubmissionByIdTest() {
+		when(abstractSubmitSubmissionRepository.findById(SUBMISSION_ID)).thenReturn(Optional.of(CoreSubmission.builder().id(SUBMISSION_ID).build()));
+		assertEquals(submissionService.getSubmitSubmissionById(SUBMISSION_ID), CoreSubmission.builder().id(SUBMISSION_ID).build());
+	}
+
+	@Test
+	void getSubmissionIdByCoreIdTest() {
+		when(submissionRepository.findByCoreSubmissionId(SUBMISSION_ID)).thenReturn(Optional.of(CoreSubmission.builder().id(SUBMISSION_ID).build()));
+		assertEquals(submissionService.getSubmissionIdByCoreId(SUBMISSION_ID), SUBMISSION_ID);
+	}
+
+	@Test
+	void getCoreSubBySubmitSubIdTest() {
+		CoreSubmission coreSubmission = CoreSubmission.builder().id(SUBMISSION_ID).build();
+		when(submissionRepository.findById(SUBMISSION_ID)).thenReturn(Optional.of(coreSubmission));
+		assertEquals(submissionService.getCoreSubBySubmitSubId(SUBMISSION_ID), Optional.of(coreSubmission));
+	}
+
+	@Test
+	void createTestSubmissionForAssignmentTest() {
+		TestSubmission testSubmission = TestSubmission.builder().id(SUBMISSION_ID).assignmentId(ASSIGNMENT_ID)
+				.build();
+		when(testSubmissionRepository.save(any())).thenReturn(testSubmission);
+		TestSubmission submission = submissionService.createTestSubmissionForAssignment(ASSIGNMENT_ID);
+		verify(testSubmissionRepository).save(argThat(sub -> sub.getAssignmentId().equals(ASSIGNMENT_ID) &&
+				sub.getCreatedDate() != null));
+		assertEquals(testSubmission, submission);
+	}
+
+	@Test
+	void getLatestTestSubmissionByAssignmentTest() {
+		TestSubmission testSubmission = TestSubmission.builder().id(1L).assignmentId(ASSIGNMENT_ID).build();
+		TestSubmission testSubmission2 = TestSubmission.builder().id(2L).assignmentId(ASSIGNMENT_ID).build();
+		when(testSubmissionRepository.findAllByAssignmentId(anyLong()))
+				.thenReturn(List.of(testSubmission, testSubmission2));
+		assertEquals(submissionService.getLatestTestSubmissionByAssignment(ASSIGNMENT_ID),
+				Optional.of(testSubmission2));
+	}
+
 	@Test
 	void downloadOneSubmission() throws IOException {
+		when(submissionRepository.findByCoreSubmissionId(anyLong())).thenReturn(Optional.of(CoreSubmission.builder().id(SUBMISSION_ID).build()));
 		submissionService.downloadSubmissions(List.of(SUBMISSION_ID));
 
 		verify(fileService).downloadFiles(SUBMISSION_ID, storageDir + File.separator + "submissions",
 				tempStorageDir + File.separator + "downloads");
+		verify(submissionRepository).findByCoreSubmissionId(SUBMISSION_ID);
 	}
 
 	@Test
@@ -440,6 +487,8 @@ class SubmissionServiceTest {
 		doCallRealMethod().when(fileService).deleteDirectory(any());
 		when(fileService.getDirectory(anyLong())).thenReturn("s1").thenReturn("s2");
 		when(fileService.toFileName(anyString())).thenCallRealMethod();
+		when(submissionRepository.findByCoreSubmissionId(SUBMISSION_ID)).thenReturn(Optional.of(CoreSubmission.builder().id(SUBMISSION_ID).build()));
+		when(submissionRepository.findByCoreSubmissionId(SUBMISSION_ID+1)).thenReturn(Optional.of(CoreSubmission.builder().id(SUBMISSION_ID+1).build()));
 
 		File dir1 = new File(storageDir + File.separator + "submissions" + File.separator + "s1"
 				+ File.separator + SUBMISSION_ID);
@@ -454,7 +503,8 @@ class SubmissionServiceTest {
 		file2.createNewFile();
 
 		submissionService.downloadSubmissions(List.of(SUBMISSION_ID, SUBMISSION_ID + 1));
-
+		verify(submissionRepository).findByCoreSubmissionId(SUBMISSION_ID);
+		verify(submissionRepository).findByCoreSubmissionId(SUBMISSION_ID+1);
 		verify(fileService).saveFilesToZip(any(),
 				eq(tempStorageDir + File.separator + "downloads" + File.separator + "submissions.zip"));
 	}
diff --git a/src/test/java/nl/tudelft/submit/service/grading/GradeServiceTest.java b/src/test/java/nl/tudelft/submit/service/grading/GradeServiceTest.java
index feb17eb0ad41dbe7c0eca685585d1643acce0b16..f16d46bb7d0c00a261b081ec96afadad16d9ccca 100644
--- a/src/test/java/nl/tudelft/submit/service/grading/GradeServiceTest.java
+++ b/src/test/java/nl/tudelft/submit/service/grading/GradeServiceTest.java
@@ -340,6 +340,11 @@ public class GradeServiceTest {
 				.isEqualTo("assignment_1__2__3_bonus_");
 	}
 
+	@Test
+	public void generateVariableNameNumberInFront() {
+		assertThat(gradeService.generateVariableName("1_Assignment 1")).isEqualTo("1_assignment_1");
+	}
+
 	@Test
 	public void passedToFormulaVariable() {
 		GradeSummaryDTO score = new GradeSummaryDTO()
diff --git a/src/test/java/nl/tudelft/submit/startup/PublishAllAssignmentsServiceTest.java b/src/test/java/nl/tudelft/submit/startup/PublishAllAssignmentsServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..3b02f60ab7fbf02a6aad9f0555b92a32d037d071
--- /dev/null
+++ b/src/test/java/nl/tudelft/submit/startup/PublishAllAssignmentsServiceTest.java
@@ -0,0 +1,75 @@
+/*
+ * Submit
+ * Copyright (C) 2020 - Delft University of Technology
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.submit.startup;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import nl.tudelft.labracore.lib.api.GradeScheme;
+import nl.tudelft.submit.model.SubmitAssignment;
+import nl.tudelft.submit.repository.SubmitAssignmentRepository;
+
+public class PublishAllAssignmentsServiceTest {
+
+	private PublishAllAssignmentsService service;
+
+	@MockBean
+	private SubmitAssignmentRepository assignmentRepository;
+
+	SubmitAssignment assignment1, assignment2;
+
+	@BeforeEach
+	void setUp() {
+		assignmentRepository = Mockito.mock(SubmitAssignmentRepository.class);
+		service = new PublishAllAssignmentsService(assignmentRepository);
+		assignment1 = SubmitAssignment.builder()
+				.assignmentId(1L)
+				.scoreType(GradeScheme.DUTCH_GRADE)
+				.hidden(false)
+				.build();
+
+		assignment2 = SubmitAssignment.builder()
+				.assignmentId(2L)
+				.scoreType(GradeScheme.DUTCH_GRADE)
+				.hidden(true)
+				.build();
+	}
+
+	@Test
+	public void testPublishAllJobOffers() {
+		List<SubmitAssignment> assignments = List.of(assignment1, assignment2);
+		when(assignmentRepository.findAll()).thenReturn(assignments);
+		assertTrue(assignment2.getHidden());
+
+		service.publishAllAssignments();
+
+		verify(assignmentRepository).findAll();
+
+		assertFalse(assignment1.getHidden());
+		assertFalse(assignment2.getHidden());
+	}
+}
diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml
index c83240f28391c5f54945a181948e8368419b6cac..8c13b541f1624fbee4c67ba845b884d6fbe0511c 100644
--- a/src/test/resources/application.yaml
+++ b/src/test/resources/application.yaml
@@ -34,6 +34,8 @@ submit:
     timeout: 1800
   cache:
     person-timeout: 60
+  startup:
+    publish-all-assignments: false
 
 spring:
   h2: