diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ac4d11a6d8aeb738cfd024f0d1d39ca9cf2613ab..9d8d689877ef0b4f9dfca686501cadeff1e68544 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -506,17 +506,19 @@ dast:
   extends:
     - .build_cached
     - .gitlab_reporter
-  tags:
-    - longJob
+  dependencies: []  # to avoid downloading artifacts
   rules:
     - if: $CI_PIPELINE_SOURCE == "trigger" ||
         $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train"
       when: never
     - if: $CI_COMMIT_BRANCH == "dev"
-      when: manual
   stage: live report
   variables:
     DAST_VERSION: latest
+    DAST_WEBSITE: https://tam.eiptest.ewi.tudelft.nl
+  needs:
+    - job: deploy_staging
+    - job: gradle_build
 
 # Run the secret detection security check and reporter.
 secret_detection:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 230b7ce9bdec1600ad5a318e866babda183ad722..822f9ad7e2ffb48997557f4e1e91a2d31f74c2c0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,11 +10,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 * Added academic periods and levels to job offers. @toberhuber
 * Added filtering by academic periods. @toberhuber
+- Index page for the TAM help center @toberhuber
+
+### Changed
+- Contract requests renamed to contract offers @toberhuber
+- TAM help center improved @toberhuber
+
+### Fixed
+
+## [2.2.2]
+
+### Added
 - Aria labels to all buttons/links used as buttons. @toberhuber
 - Show salary scale on search people page @dsavvidi
+- Show NetID in TA Training Export @dsavvidi
+- Coordinators can now filter applications on status, and contract start date. @rwbackx
+- Trainings can now be 'required' for a job offer. This will ease administration of trainings. @rwbackx
+- Default trainings can be configures for a programme. These are required for any job offer of the programme. @rwbackx
 
 ### Changed
 - Job offer applications are sorted based on the time of application @dsavvidi
+- The coordinator view for applications has been split into 'all applications' and 'for flexdelft'. @rwbackx
 
 ### Fixed
 - Declaring extra work explanation now limited to 10-140 characters @dsavvidi
@@ -31,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Fixed
 - Made all text dynamic instead of hard-coded. @toberhuber
+- Fixed HeadTA role being overwritten on accepting a job offer. @toberhuber
 - Exporting contract requests now has batches. @rwbackx
 - Toggling state now saved on Job Offers Page for Students @dsavvidi
 - Retracting a contract request gave a constraint violation exception @dsavvidi
@@ -38,6 +55,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ## [2.1.6]
 
 ### Added
+- Email notifications for unhandled contract requests. @toberhuber
+- Email notifications for unhandled raise requests. @toberhuber
 - Add pagination to raise requests, all declarations, and all applications. @toberhuber
 - Ability to select multiple applications to reject/offer. @dsavvidi
 
diff --git a/build.gradle.kts b/build.gradle.kts
index 2b4f3500be47e032bb3700d36339665c31cbea49..04b6801e7e9f70a420ca40bf73aa1f952c23f2e5 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -4,13 +4,13 @@ import nl.javadude.gradle.plugins.license.DownloadLicensesExtension
 import nl.javadude.gradle.plugins.license.LicenseExtension
 
 group = "nl.tudelft.tam"
-version = "2.2.1"
+version = "2.2.2"
 
 val javaVersion = JavaVersion.VERSION_17
 
 val labradoorVersion = "1.4.1"
 val libradorVersion = "1.3.0"
-val chihuahUIVersion = "1.0.0"
+val chihuahUIVersion = "1.2.0"
 val guavaVersion = "32.1.1-jre"
 val modelMapperVersion = "3.1.0"
 val jQueryVersion = "3.6.2"
diff --git a/docs/docs/TAM_for_Students/0_Your_Profile.md b/docs/docs/TAM_for_Students/0_Your_Profile.md
index 9869f55256c95a15d7d3b9cbee0c5635808b2d8d..096ffec8e40da738fc62d3e67bff537d54f03241 100644
--- a/docs/docs/TAM_for_Students/0_Your_Profile.md
+++ b/docs/docs/TAM_for_Students/0_Your_Profile.md
@@ -2,8 +2,12 @@
 
 All personal information related to TAing can be found on your profile.
 The profile page is available by clicking the person icon in the top right of any page.
-Here you can change some preferences like notification preferences and polo size and gender, 
-and see your payscale and which TA trainings you have completed.
+Here you can change some preferences like notification preferences and polo size and gender; 
+and see your payscale, TA experience, documents, and which TA trainings you have completed.
+
+**Note**: Experience is based Queue/Submit (i.e. TA/Head TA role in Queue) and TAM, meaning if you were not
+given TA or Head TA roles in any Labrador systems (such as Queue/Submit), your experience will not show up
+here. **We are not able to manually add experience to your profile.**
 
 ## Requesting a Higher Payscale
 
@@ -19,4 +23,22 @@ Your scale is determined by the amount of ECs obtained (see table below).
 Once you have achieved enough ECs to move to the next payscale, 
 you can request a higher payscale on your profile page.
 Click 'Request higher payscale' and upload proof from MyTUDelft.
-A TA coordinator will then look at your request and as soon as they approve it, your payscale will be updated on your profile.
+A TA coordinator will then look at your request and as soon as they approve it,
+your payscale will be updated on your profile.
+
+## Documents
+
+You can view and accept documents (such as the Code of Conduct) on your profile.
+To view a document click on the 'View' button to the right of the document name,
+this should open a PDF in your browser, in case your browser is experiencing issues
+displaying the PDF, you can download the document by clicking the 'Download' button.
+
+### Accepting Documents
+
+You may be required to accept certain documents prior to being able to apply for a job.
+In order to do this, click the 'View and Accept' button to the right of the document name,
+then read the document through and click the 'Accept' button at the bottom of the page.
+
+Once you have accepted a document, it is marked as accepted on your profile page along with a
+timestamp of when you accepted it. It is not possible to change the acceptance status of a document.
+
diff --git a/docs/docs/TAM_for_Students/1_Job_Offers.md b/docs/docs/TAM_for_Students/1_Job_Offers.md
index 54fcda905c8966390e32dd957aa8084a0357ece3..06897c81dc3b27e311f949b4d7bee34b88919c97 100644
--- a/docs/docs/TAM_for_Students/1_Job_Offers.md
+++ b/docs/docs/TAM_for_Students/1_Job_Offers.md
@@ -3,23 +3,26 @@
 If you are looking to apply for a TA job,
 you can go to the job offers page.
 Once you select a programme, you will see a list of courses with open job offers.
-To apply for a job offer, click the 'Apply button'.
-Before you apply, you will see the job information, 
-which might require you to fill out a short response.
+To apply for a job offer, click the 'View offer' button.
+Here you will see the job information, 
+which might require you to fill out a short response. Do this and click 'Apply'.
 
 ## Getting Accepted
 
 If you get accepted for a job offer, 
-you will see the status change to 'Offered' in the 'My Applications' page.
-You will still need to accept the job offer to change the status to 'Accepted'.
-In case you have no time or do not want to accept the job offer,
-you can still retract your application.
-If the position was already offered to you,
-please inform the teacher of the course that you have retracted your application.
+you will see the status change to 'Offered' in the 'My Applications' page. In addition,
+you may see a new 'Hiring Message' from the teacher, read this as it may contain more
+information regarding your job offer.
+You will still need to press 'Accept' to formally accept the job offer and to change the status to 'Accepted'.
+
+In case you have no time or do not want to accept the job offer, but have not been
+offered the position yet, you can still retract your application. 
+If the position was already offered to you, you can reject the offer, but note that
+if you apply again you will no longer be considered for the position.
 
 ## Getting Rejected
 
 It can happen that you get rejected for a job offer.
 In this case, you can view the rejection message on the 'My applications' page.
 This message should explain why you got rejected.
-Please do *not* email the teacher asking for an explanation.
\ No newline at end of file
+Please do **not** email the teacher asking for an explanation.
\ No newline at end of file
diff --git a/docs/docs/TAM_for_Students/2_Contract_Requests.md b/docs/docs/TAM_for_Students/2_Contract_Requests.md
index c2ec180ea7495d1d81ca8464bebaab5a67e7a698..faa0d01abdc0120685b9c8d0d0abcdcbd3a7bca4 100644
--- a/docs/docs/TAM_for_Students/2_Contract_Requests.md
+++ b/docs/docs/TAM_for_Students/2_Contract_Requests.md
@@ -1 +1,25 @@
-# Contract Requests
+# Contract Offers
+
+Contract offers are separate contract from the regular contracts that you receive
+for working for a course. These contracts are for extra work, such as grading or
+exam invigilation.
+
+To view the contract offers for your course, click the 'Contract Offers' tab in the
+navigation bar. Here you can see all the contract offers that have been created for
+your course.
+
+## Requesting a Contract
+
+To request a contract for your hours worked, click the 'Request Contract' button,
+fill in the form:
+ - Time Worked: The amount of hours you worked (rounded to 15 minutes in your favour).
+ - Explanation: A short explanation of the work you did.
+
+Your teacher will then approve or reject your request.
+
+## Retracting a Contract Request
+
+If you made a mistake in your contract request, you can retract it by clicking the
+'Retract' button next to the contract request in the 'My Contract Requests' tab.
+
+Your teacher will not be able to see the retracted contract request.
\ No newline at end of file
diff --git a/docs/docs/TAM_for_Teachers/0_New_to_TAM.md b/docs/docs/TAM_for_Teachers/0_New_to_TAM.md
index d6bf23607ca84dfab428928a4a648ad8391b0eb4..125ab31d1303d32c9a9559b664a89377b3646cbc 100644
--- a/docs/docs/TAM_for_Teachers/0_New_to_TAM.md
+++ b/docs/docs/TAM_for_Teachers/0_New_to_TAM.md
@@ -3,9 +3,9 @@
 TAM is a TA Management system. 
 TAM can help you simplify several tasks including but not limited to:
 
- - [Creating a job offer](/labrador/tam/tam/TAM_for_Teachers/1_Creating_a_Job_Offer)
- - [Selecting TAs](/labrador/tam/tam/TAM_for_Teachers/2_Selecting_TAs)
- - [Creating extra work](/labrador/tam/tam/TAM_for_Teachers/3_Creating_Extra_Work)
+ - [Creating a job offer](/labrador/tam/TAM_for_Teachers/1_Creating_a_Job_Offer)
+ - [Selecting TAs](/labrador/tam/TAM_for_Teachers/2_Selecting_TAs)
+ - [Creating contract offers](/labrador/tam/TAM_for_Teachers/3_Creating_Contract_Offers)
 
 ## Creating a Course Edition
 
@@ -13,6 +13,7 @@ If this is your first time using TAM, you probably want to create a course editi
 A course edition represents one iteration of your course, for example 'Object Oriented Programming *23/24 Q1*'.
 To create a course edition, navigate to the 'Manager Portal' using the header.
 If this page looks as follows, you do not have any course editions yet:
+
 ![](./no_editions.png)
 
 If there is already an upcoming edition here, an edition was created for you.
@@ -29,4 +30,4 @@ The following fields need to be filled in:
  - End date: Pick a date a bit after the last resit of your course.
  - Enrollment policy: This does not matter for TAM, we recommend selecting 'Open'.
 
-Now that you have an edition you can [Create your first job offer](/labrador/tam/tam/TAM_for_Teachers/1_Creating_a_Job_Offer).
\ No newline at end of file
+Now that you have an edition you can [Create your first job offer](/labrador/tam/TAM_for_Teachers/1_Creating_a_Job_Offer).
\ No newline at end of file
diff --git a/docs/docs/TAM_for_Teachers/1_Creating_a_Job_Offer.md b/docs/docs/TAM_for_Teachers/1_Creating_a_Job_Offer.md
index 02bcc8fc6daad4021553eda3ad391cb81b4e70f3..ca337ef70efdc31a20336e558b087b6acca8d995 100644
--- a/docs/docs/TAM_for_Teachers/1_Creating_a_Job_Offer.md
+++ b/docs/docs/TAM_for_Teachers/1_Creating_a_Job_Offer.md
@@ -12,22 +12,20 @@ and not as a 'self study session TA', then two job offers would be better:
 Job offers are only for regular work done throughout the course.
 If there is some task which is done once by some TAs, 
 e.g. grading, creating assignments before the course starts, invigilating the exam,
-you should [create extra work](/labrador/tam/tam/TAM_for_Teachers/3_Creating_Extra_Work) instead.
+you should [create contract offers](/labrador/tam/TAM_for_Teachers/3_Creating_Contract_Offers) instead.
 
 To create a new job offer, navigate to the manager portal, 
 then click 'add' next to 'Job Offers' for your course.
 You will see the following form:
 
-**PICTURE**
-[comment]: <> (TODO add picture)
-
+![](./job_offer_create_form.png)
 
 Most of these fields are self-explanatory,
 and likely your TA coordinator has already filled in a lot of this information for you.
 You might want to change the following fields:
 
  - Job description: This is the title of the job offer students will see.
-  A short description here will suffice, for example 'TA', 'Mentor', or 'Group supervisor'.
+  A short description here will suffice, for example 'TA', 'Mentor', or 'Group Supervisor'.
  - Job information: This is a longer piece of information about your job offer.
   Students see this before they apply, and they can react to it when applying.
   Markdown in this field is supported.
@@ -40,4 +38,28 @@ You might want to change the following fields:
  - Contract start and end date: This is information for FlexDelft. 
   Students can declare hours between the start and end date.
  - Deadline for applying: If there is a deadline for applying, you can check this box and fill in a deadline.
+ - Make draft: If you check this box, the job offer will not be visible to students.
+  This is useful if you want to create the job offer first and then make it visible later.
+
+After you have filled in the form, press 'Save'. You can then view your new job offer
+by clicking its name in the manager portal. This will lead you to the job offer's specific page.
+
+On this page you can see the job offer's information, and several additional options:
+
+![](./job_offer_training_messages_info.png)
+
+In general, these fields will already be pre-filled for you by your TA coordinator.
+However, you may want to change the following:
+
+ - Job Information: This can provide more details about the job to the student
+(e.g. scheduling requirements, specific tasks, etc.). This field supports Markdown.
+You may also require the student to reply to this message when applying.
+ - Hiring Message: This is a message that will be shown to students when they are offered the job.
+For instance, you may inform them of the day of the week that they would be working.
+ - Rejection Message: This is a message that will be shown to students when they are rejected.
+ - Required Trainings: If there are any trainings that TAs need to follow before they can start working,
+you can add them here. You can add a training by selecting one and clicking 'Add'. Similarly, you can
+remove a training by clicking 'Remove' next to the specified training.
+
+
 
diff --git a/docs/docs/TAM_for_Teachers/2_Selecting_TAs.md b/docs/docs/TAM_for_Teachers/2_Selecting_TAs.md
index 0c523e27f4f7a4c82e0c8d496def2b9e2572d482..3a3a192d673ce05f56a7ef9ea41aab62a2bd9a35 100644
--- a/docs/docs/TAM_for_Teachers/2_Selecting_TAs.md
+++ b/docs/docs/TAM_for_Teachers/2_Selecting_TAs.md
@@ -1,8 +1,7 @@
 # Selecting TAs
 
-After students apply to your job offer,
-you can see their applications on the job offer page.
-You can get their by clicking any job offer's name from the manager portal.
+After students apply to your job offer, you can see their applications on the job offer page.
+You can get there by clicking any job offer's name from the manager portal.
 From this page you can reject any application or offer any student the position.
 If you prefer having an overview in a spreadsheet,
 you can download the CSV of applications by clicking 'Export applications'.
@@ -14,15 +13,15 @@ Note that this information is only accurate if your course has used any Labrador
 The simplest way to pick out TAs is to go through the list of applications and,
 in combination with your own administration (e.g. previous TAs, grades),
 select which students you would like to offer the position to.
-You can do so using the 'Send offer' button in the list of applications.
+You can do so using the 'Send Offer' button in the list of applications.
 
 ## Accepting Applications in Bulk
 Some people prefer to work in spreadsheets and will get out a list of TAs they want to hire that way.
 This workflow is supported in TAM.
-You can press the 'Offer positions' button to send offers in bulk.
+You can press the 'Offer Positions' button to send offers in bulk.
 Paste the list of NetIDs in the search box, and press search.
 Double check whether you would like to offer a position to everyone in the list
-and press 'Send offer to all'.
+and press 'Send Offer to All'.
 Note that this feature also allows you to send an offer to a student who did not apply.
 In case you want to make use of this, please make sure the student is aware of this.
 
@@ -34,4 +33,4 @@ Do **not** reject students yet if you might still want to hire them,
 for example if some of the students you picked reject the offer.
 After you have assembled your complete team of TAs, you should reject all other offers,
 so they are informed they did not get the position.
-You can do so with the 'Reject all open applications' button.
\ No newline at end of file
+You can do so with the 'Reject All Open Applications' button.
\ No newline at end of file
diff --git a/docs/docs/TAM_for_Teachers/3_Creating_Extra_Work.md b/docs/docs/TAM_for_Teachers/3_Creating_Contract_Offers.md
similarity index 64%
rename from docs/docs/TAM_for_Teachers/3_Creating_Extra_Work.md
rename to docs/docs/TAM_for_Teachers/3_Creating_Contract_Offers.md
index 8e0f724ddded4962575aba962dccc268a99c287c..ac4c9fa0b9c7cd01a7cc301bb54f5eb3bf3719ee 100644
--- a/docs/docs/TAM_for_Teachers/3_Creating_Extra_Work.md
+++ b/docs/docs/TAM_for_Teachers/3_Creating_Contract_Offers.md
@@ -1,21 +1,23 @@
-# Creating Extra Work
+# Creating Contract Offers
 
-Extra work represents separate contracts from the other regular contracts for your course.
+Contract offers represent separate contracts from the other regular contracts for your course.
 The main use case is grading work.
-To create extra work, click the 'Add' button next to 'Extra work' for your course.
+To create a contract offer, click the 'Add' button next to 'Contract Offers' for your course.
 
 The following fields will need to be filled in:
 
 - Work description: This is the title of the extra work students will see.
-  A short description here will suffice, for example 'Grading', 'Assignment creation'.
+  A short description here will suffice, for example 'Grading', 'Assignment Creation'.
 - Contract name: This is the name of the contract as will be sent to FlexDelft.
 - Baancode (Contract code): If you know the baancode for your contract you can set it,
   but your TA coordinator might have already set a baancode for you.
   If you are not sure what to enter here, and it is not filled in for you, contact your TA coordinator.
+- Max hours: The maximum amount of hours TAs can declare in total on this contract.
+  It is recommended to set this slightly higher than the expected maximum amount of hours.
 - Deadline for contract requests: If there is a deadline for requesting a contract, 
   you can check this box and fill in a deadline.
 
-You can click on an extra work entry to see all contracts requested.
+You can click on a contract offer entry to see all contracts requested.
 On this page, you can see the amount declared per student and their explanation.
 If you approve a contract request, 
 the TA coordinator will see it in their export of contracts to send to FlexDelft.
diff --git a/docs/docs/TAM_for_Teachers/job_offer_create_form.png b/docs/docs/TAM_for_Teachers/job_offer_create_form.png
new file mode 100644
index 0000000000000000000000000000000000000000..0352428ad3c9a31e147d8471aa7e276a38641762
Binary files /dev/null and b/docs/docs/TAM_for_Teachers/job_offer_create_form.png differ
diff --git a/docs/docs/TAM_for_Teachers/job_offer_training_messages_info.png b/docs/docs/TAM_for_Teachers/job_offer_training_messages_info.png
new file mode 100644
index 0000000000000000000000000000000000000000..027d736ddbf252931e0d78691fc9e04d5dd0b904
Binary files /dev/null and b/docs/docs/TAM_for_Teachers/job_offer_training_messages_info.png differ
diff --git a/docs/docs/index.md b/docs/docs/index.md
new file mode 100644
index 0000000000000000000000000000000000000000..017f05fd33ed3a519cbae65b045b5b47e00dadca
--- /dev/null
+++ b/docs/docs/index.md
@@ -0,0 +1,7 @@
+# TAM - Teaching Assistant Management
+
+## Are you a student?
+Head to the [student page](/labrador/tam/TAM_for_Students/0_Your_Profile/) to learn more about how TAM can help you.
+
+## Are you a teacher?
+Head to the [teacher page](/labrador/tam/TAM_for_Teachers/0_New_to_TAM/) to learn more about how TAM can help you.
\ No newline at end of file
diff --git a/src/main/java/nl/tudelft/tam/DevDatabaseLoader.java b/src/main/java/nl/tudelft/tam/DevDatabaseLoader.java
index e865006471a010bd5350f837363f2061b1c8ab5e..6caaf6944a1d09f2a2c3288d08de4680d6022ca1 100644
--- a/src/main/java/nl/tudelft/tam/DevDatabaseLoader.java
+++ b/src/main/java/nl/tudelft/tam/DevDatabaseLoader.java
@@ -23,6 +23,7 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.time.LocalDate;
 import java.util.Comparator;
+import java.util.Set;
 
 import javax.annotation.PostConstruct;
 import javax.transaction.Transactional;
@@ -83,6 +84,8 @@ public class DevDatabaseLoader {
 	final long ID_ADS_NOW = 4L;
 	final long ID_SQT_NOW = 6L;
 
+	TrainingType regularCSE;
+
 	JobOffer oop_now_jo_ta;
 	JobOffer oop_now_jo_mentor;
 	JobOffer ads_now_jo_ta;
@@ -116,13 +119,13 @@ public class DevDatabaseLoader {
 	 */
 	@PostConstruct
 	private void init() throws IOException {
+		initTrainingApprovals();
 		initJobOffers();
 		initExtraWork();
 		fetchPeople();
 		initApplications();
 		initDeclarations();
 		initRaiseRequests();
-		initTrainingApprovals();
 	}
 
 	private void initJobOffers() {
@@ -134,6 +137,7 @@ public class DevDatabaseLoader {
 				.contractEndDate(LocalDate.now().plusWeeks(2))
 				.baanCode("OOP TA BAAN CODE")
 				.maxHours(100)
+				.requiredTrainings(Set.of(regularCSE))
 				.hiringMessage("Could you provide us with the following information: "
 						+ "Are you available on Tuesdays and/or Fridays? "
 						+ "What is the maximum of hours you are available?")
@@ -148,6 +152,7 @@ public class DevDatabaseLoader {
 				.contractEndDate(LocalDate.now().plusWeeks(2))
 				.baanCode("OOP Mentor BAAN CODE")
 				.maxHours(100)
+				.requiredTrainings(Set.of(regularCSE))
 				.hiringMessage("This is a different, shorter, hiring message!")
 				.deadline(LocalDate.now().plusDays(7))
 				.period(AcademicPeriod.S1)
@@ -161,6 +166,7 @@ public class DevDatabaseLoader {
 				.contractEndDate(LocalDate.now().plusWeeks(2))
 				.baanCode("ADS TA BAAN CODE")
 				.maxHours(100)
+				.requiredTrainings(Set.of(regularCSE))
 				.deadline(LocalDate.now().plusDays(7))
 				.period(AcademicPeriod.Q2)
 				.level(AcademicLevel.BACHELOR1)
@@ -173,6 +179,7 @@ public class DevDatabaseLoader {
 				.contractEndDate(LocalDate.now().plusWeeks(2))
 				.baanCode("ADS Mentor BAAN CODE")
 				.maxHours(100)
+				.requiredTrainings(Set.of(regularCSE))
 				.rejectMessage("Sorry :(")
 				.period(AcademicPeriod.SUMMER)
 				.level(AcademicLevel.BACHELOR1)
@@ -185,6 +192,7 @@ public class DevDatabaseLoader {
 				.contractEndDate(LocalDate.now().plusWeeks(2))
 				.baanCode("SQT TA BAAN CODE")
 				.maxHours(100)
+				.requiredTrainings(Set.of(regularCSE))
 				.period(AcademicPeriod.Q4)
 				.level(AcademicLevel.BACHELOR1)
 				.build());
@@ -196,6 +204,7 @@ public class DevDatabaseLoader {
 				.contractEndDate(LocalDate.now().plusWeeks(2))
 				.baanCode("SQT Mentor BAAN CODE")
 				.maxHours(100)
+				.requiredTrainings(Set.of(regularCSE))
 				.period(AcademicPeriod.Q4)
 				.level(AcademicLevel.BACHELOR3)
 				.deadline(LocalDate.now().minusDays(1))
@@ -326,8 +335,9 @@ public class DevDatabaseLoader {
 	}
 
 	private void initTrainingApprovals() {
-		TrainingType regularCSE = TrainingType.builder().name("Regular Training").programId(1L).build();
-		TrainingType regularEE = TrainingType.builder().name("Regular Training").programId(2L).build();
+		regularCSE = TrainingType.builder().name("Regular Training").isDefault(true).programId(1L).build();
+		TrainingType regularEE = TrainingType.builder().name("Regular Training").isDefault(true).programId(2L)
+				.build();
 		TrainingType team = TrainingType.builder().name("Team Training").programId(1L).build();
 
 		trainingTypeRepository.save(regularCSE);
diff --git a/src/main/java/nl/tudelft/tam/config/EmailConfig.java b/src/main/java/nl/tudelft/tam/config/EmailConfig.java
index 94eb3a3cec8023171d42ccd5f5ca678f4d2d4bec..ca2df066a67cde60f5aefcc86f444405c09ce4fd 100644
--- a/src/main/java/nl/tudelft/tam/config/EmailConfig.java
+++ b/src/main/java/nl/tudelft/tam/config/EmailConfig.java
@@ -25,6 +25,7 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.mail.javamail.JavaMailSender;
 import org.springframework.mail.javamail.JavaMailSenderImpl;
+import org.springframework.scheduling.annotation.EnableScheduling;
 import org.thymeleaf.TemplateEngine;
 import org.thymeleaf.spring5.SpringTemplateEngine;
 import org.thymeleaf.templatemode.TemplateMode;
@@ -33,6 +34,7 @@ import org.thymeleaf.templateresolver.ITemplateResolver;
 import org.thymeleaf.templateresolver.StringTemplateResolver;
 
 @Configuration
+@EnableScheduling
 public class EmailConfig {
 
 	@Value("${spring.mail.default-encoding}")
diff --git a/src/main/java/nl/tudelft/tam/controller/ApplicationController.java b/src/main/java/nl/tudelft/tam/controller/ApplicationController.java
index 16191ee70418fe698a5b68c4baf62822a816cce1..fab45297b569c6644a68c1966786320153df4f1a 100644
--- a/src/main/java/nl/tudelft/tam/controller/ApplicationController.java
+++ b/src/main/java/nl/tudelft/tam/controller/ApplicationController.java
@@ -20,14 +20,11 @@ package nl.tudelft.tam.controller;
 import java.io.IOException;
 import java.time.LocalDateTime;
 import java.util.*;
-import java.util.stream.Collectors;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.io.Resource;
-import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.web.PageableDefault;
-import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Controller;
@@ -39,6 +36,7 @@ import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
 import nl.tudelft.labracore.lib.security.user.Person;
 import nl.tudelft.tam.controller.utility.PageUtil;
 import nl.tudelft.tam.dto.create.ApplicationCreateDTO;
+import nl.tudelft.tam.dto.util.ApplicationFilterDTO;
 import nl.tudelft.tam.dto.view.details.ApplicationDetailsJobOfferDTO;
 import nl.tudelft.tam.enums.AcademicPeriod;
 import nl.tudelft.tam.enums.Status;
@@ -70,44 +68,60 @@ public class ApplicationController {
 	/**
 	 * Gets the coordinator page.
 	 *
-	 * @param  person  The authenticated person
-	 * @param  program The program to see the applications for
-	 * @param  since   The applications changed since this time
-	 * @param  model   The model to add data to
-	 * @return         The coordinator page
+	 * @param  person The authenticated person
+	 * @param  filter The filter to apply to the applications
+	 * @param  model  The model to add data to
+	 * @return        The coordinator page
 	 */
 	@GetMapping("all")
 	@PreAuthorize("@authorisationService.isCoordinatorOfAny()")
 	public String getAllApplications(@AuthenticatedPerson Person person,
-			@RequestParam(required = false) Long program,
-			@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime since,
+			ApplicationFilterDTO filter,
 			@PageableDefault(value = 25) Pageable pageable,
 			Model model) {
 		List<ProgramDetailsDTO> programs = programService.getCoordinatingPrograms(person.getId());
-		if (program == null) {
-			program = programs.get(0).getId();
+		if (filter.getProgram() == null) {
+			filter.setProgram(programs.get(0).getId());
 		}
 
 		List<Application> applications = applicationService
-				.getCoordinatingApplicationsForProgram(person.getId(), program)
-				.stream()
-				.filter(a -> !editionService.getOrThrow(a.getJobOffer().getEditionId()).getIsArchived())
-				.collect(Collectors.toList());
-		if (since != null) {
-			applications = applicationService.filterApplicationsChangedSince(applications, since);
-		}
+				.getFilteredCoordinatingApplications(person.getId(), filter);
 
-		// Exclude retracted applications
-		applications = applications.stream().filter(x -> !x.getStatus().equals(Status.REJECTED_BY_STUDENT))
-				.collect(Collectors.toList());
+		model.addAttribute("applications", PageUtil.pageFromList(applications, pageable));
+		model.addAttribute("programs", programs);
+
+		return "application/all";
+	}
+
+	/**
+	 * Gets the coordinator page, with FlexDelft filters.
+	 *
+	 * @param  person The authenticated person
+	 * @param  filter The filter to apply to the applications
+	 * @param  model  The model to add data to
+	 * @return        The coordinator page
+	 */
+	@GetMapping("all/flexdelft")
+	@PreAuthorize("@authorisationService.isCoordinatorOfAny()")
+	public String getAllApplicationsForFlexDelft(@AuthenticatedPerson Person person,
+			ApplicationFilterDTO filter,
+			@PageableDefault(value = 25) Pageable pageable,
+			Model model) {
+		List<ProgramDetailsDTO> programs = programService.getCoordinatingPrograms(person.getId());
+		if (filter.getProgram() == null) {
+			filter.setProgram(programs.get(0).getId());
+		}
 
-		// Pagination
-		Page<Application> pageOfApplications = PageUtil.pageFromList(applications, pageable);
+		filter.setStatuses(List.of(Status.ACCEPTED));
+		List<Application> applications = applicationService
+				.getFilteredCoordinatingApplications(person.getId(), filter);
 
-		model.addAttribute("applications", pageOfApplications);
-		model.addAttribute("exports", applicationService.getExportsForPersonAndProgram(person, program));
+		model.addAttribute("applications", PageUtil.pageFromList(applications, pageable));
+		model.addAttribute("exports",
+				applicationService.getExportsForPersonAndProgram(person, filter.getProgram()));
 		model.addAttribute("programs", programs);
-		return "application/all";
+
+		return "application/all_flexdelft";
 	}
 
 	/**
@@ -143,7 +157,7 @@ public class ApplicationController {
 	 * @return          The job offer page (i.e. from where the promotion was made)
 	 */
 	@PostMapping("promote/{personId}/{offerId}")
-	@PreAuthorize("@authorisationService.isManagerOfAny()")
+	@PreAuthorize("@authorisationService.isManagerOfJob(#offerId)")
 	public String promote(@PathVariable Long personId, @PathVariable Long offerId) {
 		applicationService.promote(personId, offerId);
 		return "redirect:/job-offer/" + offerId;
@@ -157,7 +171,7 @@ public class ApplicationController {
 	 * @return          The job offer page (i.e. from where the action was taken to change)
 	 */
 	@PostMapping("demote/{personId}/{offerId}")
-	@PreAuthorize("@authorisationService.isManagerOfAny()")
+	@PreAuthorize("@authorisationService.isManagerOfJob(#offerId)")
 	public String demote(@PathVariable Long personId, @PathVariable Long offerId) {
 		applicationService.demote(personId, offerId);
 		return "redirect:/job-offer/" + offerId;
@@ -172,10 +186,11 @@ public class ApplicationController {
 	 * @return        The page to redirect the user to (either my applications or previous is provided)
 	 */
 	@PostMapping("submit")
+	@PreAuthorize("@authorisationService.canSubmitApplication(#create.jobOffer.id)")
 	public String submitApplication(@AuthenticatedPerson Person person,
 			ApplicationCreateDTO create,
 			@RequestParam(required = false) String page,
-			@RequestParam(required = false) String program,
+			@RequestParam(required = false) Long program,
 			@RequestParam(required = false, name = "period") List<AcademicPeriod> filterPeriods) {
 		applicationService.submit(person.getId(), create);
 
@@ -259,7 +274,7 @@ public class ApplicationController {
 	 * @return          The job offer page (ie. from where the offer was made)
 	 */
 	@PostMapping("offer/{personId}/{offerId}")
-	@PreAuthorize("@authorisationService.isManagerOfAny()")
+	@PreAuthorize("@authorisationService.isManagerOfJob(#offerId)")
 	public String offerApplication(@AuthenticatedPerson Person person,
 			@PathVariable Long personId,
 			@PathVariable Long offerId) {
@@ -352,15 +367,12 @@ public class ApplicationController {
 	 * @return        The export file as a resource
 	 */
 	@GetMapping("export")
-	@PreAuthorize("@authorisationService.isCoordinatorOfAny()")
-	public ResponseEntity<Resource> exportApplications(@AuthenticatedPerson Person person)
+	@PreAuthorize("@authorisationService.isCoordinatorForProgram(#filter.program)")
+	public ResponseEntity<Resource> exportApplications(@AuthenticatedPerson Person person,
+			ApplicationFilterDTO filter)
 			throws IOException {
 		return csvService.getResponse(applicationService.exportApplications("applications",
-				applicationService.getCoordinatingApplications(person.getId()).stream()
-						.filter(x -> !editionService.getOrThrow(x.getJobOffer().getEditionId())
-								.getIsArchived())
-						.filter(x -> !x.getStatus().equals(Status.REJECTED_BY_STUDENT))
-						.collect(Collectors.toList())));
+				applicationService.getFilteredCoordinatingApplications(person.getId(), filter)));
 	}
 
 	/**
@@ -370,19 +382,22 @@ public class ApplicationController {
 	 * @return        The export file as a resource
 	 */
 	@GetMapping("flex-delft-export")
-	@PreAuthorize("@authorisationService.isCoordinatorOfAny()")
+	@PreAuthorize("@authorisationService.isCoordinatorForProgram(#filter.program)")
 	public ResponseEntity<Resource> exportApplicationsForFlexDelft(@AuthenticatedPerson Person person,
+			ApplicationFilterDTO filter,
 			@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime since,
-			@RequestParam Long program, @RequestParam Integer batch) throws IOException {
+			@RequestParam Long program,
+			@RequestParam Integer batch) throws IOException {
+		filter.setStatuses(List.of(Status.ACCEPTED));
 		List<Application> appList = applicationService
 				.getCoordinatingApplicationsForProgram(person.getId(), program).stream()
 				.filter(x -> !editionService.getOrThrow(x.getJobOffer().getEditionId()).getIsArchived())
 				.filter(x -> x.getStatus().equals(Status.ACCEPTED))
 				.collect(Collectors.toList());
-
 		return csvService
-				.getResponse(applicationService.exportApplicationsForFlexDelft(person, program, batch,
-						"applications", appList, since));
+				.getResponse(
+						applicationService.exportApplicationsForFlexDelft(person, filter.getProgram(), batch,
+								"applications", appList, filter.getUpdatedSince()));
 	}
 
 }
diff --git a/src/main/java/nl/tudelft/tam/controller/EditionController.java b/src/main/java/nl/tudelft/tam/controller/EditionController.java
index 56b7e61232d5b5fa8ed28652e47a9e7f6c54eddf..8010faf700c108bc10acf10652f31c94fb816334 100644
--- a/src/main/java/nl/tudelft/tam/controller/EditionController.java
+++ b/src/main/java/nl/tudelft/tam/controller/EditionController.java
@@ -22,8 +22,6 @@ import java.time.LocalTime;
 
 import javax.validation.Valid;
 
-import org.modelmapper.ModelMapper;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Controller;
 import org.springframework.validation.annotation.Validated;
@@ -31,6 +29,7 @@ import org.springframework.web.bind.annotation.ModelAttribute;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 
+import lombok.AllArgsConstructor;
 import nl.tudelft.labracore.api.dto.EditionCreateDTO;
 import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
 import nl.tudelft.labracore.lib.security.user.Person;
@@ -39,16 +38,12 @@ import nl.tudelft.tam.service.EditionService;
 import nl.tudelft.tam.service.RoleService;
 
 @Controller
+@AllArgsConstructor
 @RequestMapping("edition")
 public class EditionController {
 
-	private final ModelMapper mapper = new ModelMapper();
-
-	@Autowired
-	EditionService editionService;
-
-	@Autowired
-	RoleService roleService;
+	private EditionService editionService;
+	private RoleService roleService;
 
 	/**
 	 * Creates a new edition, adds students if selected, and adds creator as teacher.
diff --git a/src/main/java/nl/tudelft/tam/controller/JobOfferController.java b/src/main/java/nl/tudelft/tam/controller/JobOfferController.java
index 24c8d14aa8c90fde6277ff488f9837a066aafb18..065e247d74ae0bd5d69a9edd39175917101879d2 100644
--- a/src/main/java/nl/tudelft/tam/controller/JobOfferController.java
+++ b/src/main/java/nl/tudelft/tam/controller/JobOfferController.java
@@ -98,6 +98,9 @@ public class JobOfferController {
 	@Autowired
 	private ProgramService programService;
 
+	@Autowired
+	private TrainingTypeService trainingTypeService;
+
 	@Autowired
 	private ModelMapper mapper;
 
@@ -285,6 +288,8 @@ public class JobOfferController {
 	public String getJobOfferDetails(@PathVariable Long id,
 			@RequestParam(required = false) String q, Model model) {
 		JobOfferDetailsDTO offer = jobOfferService.getById(id);
+		Long programId = courseService.getCourseById(offer.getEdition().getCourse().getId()).getProgram()
+				.getId();
 
 		List<ApplicationDetailsPersonDTO> applications = applicationService
 				.getFilteredApplicationsForJobOffer(id, q);
@@ -297,7 +302,8 @@ public class JobOfferController {
 		model.addAttribute("applications", applications.stream()
 				.sorted(Comparator.comparing(ApplicationSummaryDTO::getCreatedDate)).toList());
 		model.addAttribute("stats", stats);
-		model.addAttribute("patchJobOffer", new JobOfferPatchDTO());
+		model.addAttribute("trainingTypes",
+				trainingTypeService.getAllTrainingTypesForPrograms(List.of(programId)));
 
 		return "job_offer/view_one";
 	}
@@ -313,7 +319,7 @@ public class JobOfferController {
 	 * @return     The specific job offer page
 	 */
 	@PostMapping
-	@PreAuthorize("@authorisationService.isManagerOfAny()")
+	@PreAuthorize("@authorisationService.isManagerOfEdition(#dto.editionId)")
 	public String createJobOffer(
 			@Valid @ModelAttribute("joboffer") @DateTimeFormat(pattern = "yyyy-MM-dd") JobOfferCreateDTO dto) {
 		Long id = jobOfferService.newJobOffer(dto);
@@ -363,6 +369,30 @@ public class JobOfferController {
 		return "redirect:/job-offer/" + id;
 	}
 
+	/**
+	 * Adds a required training to a job offer.
+	 *
+	 * @param id     The id of the job offer
+	 * @param typeId The id of the training
+	 */
+	@PostMapping("{id}/required-training/{typeId}")
+	@PreAuthorize("@authorisationService.isManagerOfJob(#id)")
+	public @ResponseBody void addRequiredTraining(@PathVariable Long id, @PathVariable Long typeId) {
+		jobOfferService.addRequiredTraining(id, typeId);
+	}
+
+	/**
+	 * Removes a required training to a job offer.
+	 *
+	 * @param id     The id of the job offer
+	 * @param typeId The id of the training type
+	 */
+	@DeleteMapping("{id}/required-training/{typeId}")
+	@PreAuthorize("@authorisationService.isManagerOfJob(#id)")
+	public @ResponseBody void removeRequiredTraining(@PathVariable Long id, @PathVariable Long typeId) {
+		jobOfferService.removeRequiredTraining(id, typeId);
+	}
+
 	// endregion
 
 	// region import/export
diff --git a/src/main/java/nl/tudelft/tam/controller/TrainingTypeController.java b/src/main/java/nl/tudelft/tam/controller/TrainingTypeController.java
index a1368a1d92e1af9c26c30df98615fde5fc5b8e22..cc9a548ef12b3f8bb5196fa9623f5d194268b3c6 100644
--- a/src/main/java/nl/tudelft/tam/controller/TrainingTypeController.java
+++ b/src/main/java/nl/tudelft/tam/controller/TrainingTypeController.java
@@ -41,14 +41,10 @@ public class TrainingTypeController {
 	 * @return           The redirect to the training page.
 	 */
 	@PostMapping
-	@PreAuthorize("@authorisationService.isCoordinatorForProgram(#programId)")
-	public String createTrainingType(@RequestParam Long programId,
-			@RequestParam String name) {
-		TrainingTypeCreateDTO createDTO = TrainingTypeCreateDTO.builder().name(name).programId(programId)
-				.build();
+	@PreAuthorize("@authorisationService.isCoordinatorForProgram(#createDTO.programId)")
+	public String createTrainingType(TrainingTypeCreateDTO createDTO) {
 		trainingTypeService.createNewTrainingType(createDTO);
-
-		return "redirect:/coordinator/training/" + programId;
+		return "redirect:/coordinator/training/" + createDTO.getProgramId();
 	}
 
 	/**
@@ -63,10 +59,8 @@ public class TrainingTypeController {
 	@PreAuthorize("@authorisationService.isCoordinatorForProgram(#programId)")
 	public String updateTrainingType(@PathVariable Long programId,
 			@PathVariable Long trainingTypeId,
-			@RequestParam String name) {
-		TrainingTypePatchDTO patchDTO = TrainingTypePatchDTO.builder().name(name).build();
+			TrainingTypePatchDTO patchDTO) {
 		trainingTypeService.updateTrainingType(patchDTO, trainingTypeId);
-
 		return "redirect:/coordinator/training/" + programId;
 	}
 
diff --git a/src/main/java/nl/tudelft/tam/cron/CronService.java b/src/main/java/nl/tudelft/tam/cron/CronService.java
new file mode 100644
index 0000000000000000000000000000000000000000..428965aabba048db5812d02da4ab3d5fe848db0a
--- /dev/null
+++ b/src/main/java/nl/tudelft/tam/cron/CronService.java
@@ -0,0 +1,191 @@
+/*
+ * TAM
+ * Copyright (C) 2021 - 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.tam.cron;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import nl.tudelft.labracore.api.dto.EditionDetailsDTO;
+import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
+import nl.tudelft.labracore.api.dto.ProgramSummaryDTO;
+import nl.tudelft.labracore.api.dto.RolePersonDetailsDTO;
+import nl.tudelft.tam.dto.create.EmailCreateDTO;
+import nl.tudelft.tam.dto.view.details.RaiseRequestDetailsDTO;
+import nl.tudelft.tam.dto.view.summary.ExtraWorkSummaryDTO;
+import nl.tudelft.tam.enums.NotificationFrequency;
+import nl.tudelft.tam.model.Coordinator;
+import nl.tudelft.tam.model.Profile;
+import nl.tudelft.tam.service.*;
+
+@Service
+public class CronService {
+
+	@Autowired
+	private EmailService emailService;
+
+	@Autowired
+	private ExtraWorkService extraWorkService;
+
+	@Autowired
+	private DeclarationService declarationService;
+
+	@Autowired
+	private RoleService roleService;
+
+	@Autowired
+	private EditionService editionService;
+
+	@Autowired
+	private ProfileService profileService;
+
+	@Autowired
+	private RaiseRequestService raiseRequestService;
+
+	@Autowired
+	private ProgramService programService;
+
+	@Autowired
+	private CoordinatorService coordinatorService;
+
+	@Autowired
+	private PersonService personService;
+
+	/**
+	 * Send Accumulated Changes Weekly on a Monday at 00:00. Currently: Teachers - Declarations Aren't Handled
+	 * EditPayScale / Coordinators Generally - Raise Requests Awaiting Approval
+	 */
+	@Scheduled(cron = "0 0 0 * * 1")
+	public void sendBulkWeeklyAccumulatedChanges() {
+		remindTeachersAboutUnhandledDeclarations(NotificationFrequency.Weekly);
+		remindEditPayScalePeopleAboutAwaitingRaiseRequests(NotificationFrequency.Weekly);
+	}
+
+	/**
+	 * Send Accumulated Changes On Weekdays at 00:00. Currently: Teachers - Declarations Aren't Handled
+	 * EditPayScale / Coordinators Generally - Raise Requests Awaiting Approval
+	 */
+	@Scheduled(cron = "0 0 0 * * 1-5")
+	public void sendBulkDailyAccumulatedChanges() {
+		remindTeachersAboutUnhandledDeclarations(NotificationFrequency.Daily);
+		remindEditPayScalePeopleAboutAwaitingRaiseRequests(NotificationFrequency.Daily);
+	}
+
+	private void remindEditPayScalePeopleAboutAwaitingRaiseRequests(NotificationFrequency frequency) {
+		List<Long> programIds = programService.getAllPrograms().stream()
+				.map(ProgramSummaryDTO::getId)
+				.collect(Collectors.toList());
+
+		List<Long> programsWithAwaitingRaiseRequests = raiseRequestService.getAllSubmitted(programIds)
+				.stream()
+				.map(RaiseRequestDetailsDTO::getRelevantProgramIds)
+				.flatMap(List::stream)
+				.distinct()
+				.toList();
+
+		Map<String, String> coordinatorEmailToDisplayName = new HashMap<>();
+		for (Long programId : programsWithAwaitingRaiseRequests) {
+			List<Long> coordinatorIds = coordinatorService.getCoordinatorsByProgram(programId).stream()
+					.map(Coordinator::getId).toList();
+			List<PersonSummaryDTO> coordinators = personService.getPeopleById(coordinatorIds);
+
+			coordinators.forEach(coordinator -> {
+				profileService.findById(coordinator.getId()).ifPresent(profile -> {
+					if (profile.getEmailNotifications()
+							&& profile.getEmailNotificationFrequency().equals(frequency)) {
+						coordinatorEmailToDisplayName.put(
+								personService.getPersonById(profile.getId()).getEmail(),
+								coordinator.getDisplayName());
+					}
+				});
+			});
+		}
+
+		// Send Emails
+		for (Map.Entry<String, String> entry : coordinatorEmailToDisplayName.entrySet()) {
+			Map<String, Object> contextVars = new HashMap<>(Map.of("name", entry.getValue()));
+
+			EmailCreateDTO email = EmailCreateDTO.builder()
+					.subject("TAM - Raise Requests Pending")
+					.template("html/raise_requests_pending")
+					.contextVariables(contextVars)
+					.build();
+
+			emailService.sendEmail(List.of(entry.getKey()), email);
+		}
+	}
+
+	private void remindTeachersAboutUnhandledDeclarations(NotificationFrequency frequency) {
+		Map<String, List<String>> teacherEmailToExtraWorkList = new HashMap<>();
+		Map<String, String> teacherEmailToDisplayName = new HashMap<>();
+
+		List<ExtraWorkSummaryDTO> openWork = extraWorkService.getAllOpen();
+		for (ExtraWorkSummaryDTO work : openWork) {
+			float numberOfUnhandledHours = declarationService.getNumberOfUnhandledHours(work.getId());
+
+			if (numberOfUnhandledHours > 0) {
+				// Construct the bullet point of the extra work in the email
+				EditionDetailsDTO edition = editionService.getEditionById(work.getEditionId());
+				String extraWorkBulletPoint = edition.getCourse().getName() + "(" + edition.getName() + ") - "
+						+ work.getName() + ": " + numberOfUnhandledHours + " hours";
+
+				// Go through all teachers of the extra work
+				List<RolePersonDetailsDTO> teachers = roleService.getTeachersById(work.getEditionId());
+				for (RolePersonDetailsDTO teacher : teachers) {
+					String email = teacher.getPerson().getEmail();
+					String displayName = teacher.getPerson().getDisplayName();
+
+					// Check if notifications are enabled
+					Profile profile = profileService.findByIdOrCreate(teacher.getPerson().getId());
+					if (profile.getEmailNotifications()
+							&& profile.getEmailNotificationFrequency().equals(frequency)) {
+						if (!teacherEmailToExtraWorkList.containsKey(email)) {
+							teacherEmailToExtraWorkList.put(email, List.of(extraWorkBulletPoint));
+						} else {
+							List<String> ewList = teacherEmailToExtraWorkList.get(email);
+							ewList.add(extraWorkBulletPoint);
+							teacherEmailToExtraWorkList.put(email, ewList);
+						}
+						teacherEmailToDisplayName.put(email, displayName);
+					}
+				}
+			}
+		}
+
+		// Send Emails
+		for (Map.Entry<String, List<String>> entry : teacherEmailToExtraWorkList.entrySet()) {
+			Map<String, Object> contextVars = new HashMap<>(Map.of(
+					"name", teacherEmailToDisplayName.get(entry.getKey()),
+					"listOfBulletPoints", entry.getValue()));
+
+			EmailCreateDTO email = EmailCreateDTO.builder()
+					.subject("TAM - Contract Requests Pending")
+					.template("html/contract_requests_pending")
+					.contextVariables(contextVars)
+					.build();
+
+			emailService.sendEmail(List.of(entry.getKey()), email);
+		}
+	}
+
+}
diff --git a/src/main/java/nl/tudelft/tam/dto/create/TrainingTypeCreateDTO.java b/src/main/java/nl/tudelft/tam/dto/create/TrainingTypeCreateDTO.java
index 4a4ab4662d20abf8c183f6059cfbdb4ffd1235b6..e01642f01ed83a4777ed0ad75a4c4afe40ef7244 100644
--- a/src/main/java/nl/tudelft/tam/dto/create/TrainingTypeCreateDTO.java
+++ b/src/main/java/nl/tudelft/tam/dto/create/TrainingTypeCreateDTO.java
@@ -36,6 +36,10 @@ public class TrainingTypeCreateDTO extends Create<TrainingType> {
 	@NotNull
 	private Long programId;
 
+	@NotNull
+	@Builder.Default
+	private Boolean isDefault = false;
+
 	@Override
 	public Class<TrainingType> clazz() {
 		return TrainingType.class;
diff --git a/src/main/java/nl/tudelft/tam/dto/patch/TrainingTypePatchDTO.java b/src/main/java/nl/tudelft/tam/dto/patch/TrainingTypePatchDTO.java
index 988760a3682dfc00a1cb803745ea52bf1f6f800e..eff7b4a7564da7b97f656ca015d8c47d84230eca 100644
--- a/src/main/java/nl/tudelft/tam/dto/patch/TrainingTypePatchDTO.java
+++ b/src/main/java/nl/tudelft/tam/dto/patch/TrainingTypePatchDTO.java
@@ -17,6 +17,8 @@
  */
 package nl.tudelft.tam.dto.patch;
 
+import javax.validation.constraints.NotNull;
+
 import lombok.*;
 import nl.tudelft.librador.dto.patch.Patch;
 import nl.tudelft.tam.model.TrainingType;
@@ -30,9 +32,14 @@ public class TrainingTypePatchDTO extends Patch<TrainingType> {
 
 	private String name;
 
+	@NotNull
+	@Builder.Default
+	private Boolean isDefault = false;
+
 	@Override
 	protected void applyOneToOne() {
 		updateNonNull(name, data::setName);
+		updateNonNull(isDefault, data::setIsDefault);
 	}
 
 	@Override
diff --git a/src/main/java/nl/tudelft/tam/dto/util/ApplicationFilterDTO.java b/src/main/java/nl/tudelft/tam/dto/util/ApplicationFilterDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..472b020695aacd11a7ab50de615cedc603568f81
--- /dev/null
+++ b/src/main/java/nl/tudelft/tam/dto/util/ApplicationFilterDTO.java
@@ -0,0 +1,51 @@
+/*
+ * TAM
+ * Copyright (C) 2021 - 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.tam.dto.util;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+
+import org.springframework.format.annotation.DateTimeFormat;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import nl.tudelft.tam.enums.Status;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ApplicationFilterDTO {
+
+	private Long program;
+
+	@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
+	private LocalDateTime updatedSince;
+
+	@DateTimeFormat(pattern = "yyyy-MM-dd")
+	private LocalDate contractStartAfter;
+
+	@DateTimeFormat(pattern = "yyyy-MM-dd")
+	private LocalDate contractStartBefore;
+
+	private List<Status> statuses;
+
+}
diff --git a/src/main/java/nl/tudelft/tam/dto/view/details/JobOfferDetailsDTO.java b/src/main/java/nl/tudelft/tam/dto/view/details/JobOfferDetailsDTO.java
index 66eed6af10f0582ee29e5c58da226ff44d53eaa6..3cb30b4506821c3b4792a173e1cdeac8ec2ede9a 100644
--- a/src/main/java/nl/tudelft/tam/dto/view/details/JobOfferDetailsDTO.java
+++ b/src/main/java/nl/tudelft/tam/dto/view/details/JobOfferDetailsDTO.java
@@ -17,10 +17,13 @@
  */
 package nl.tudelft.tam.dto.view.details;
 
+import java.util.List;
+
 import lombok.*;
 import lombok.experimental.SuperBuilder;
 import nl.tudelft.labracore.api.dto.EditionDetailsDTO;
 import nl.tudelft.tam.dto.view.summary.JobOfferSummaryDTO;
+import nl.tudelft.tam.dto.view.summary.TrainingTypeSummaryDTO;
 
 @Data
 @SuperBuilder
@@ -29,5 +32,8 @@ import nl.tudelft.tam.dto.view.summary.JobOfferSummaryDTO;
 @EqualsAndHashCode(callSuper = false)
 public class JobOfferDetailsDTO extends JobOfferSummaryDTO {
 
-	private EditionDetailsDTO edition; // In theory editionSummaryDTO is enough, but .getById gives details, so it's here
+	private EditionDetailsDTO edition;
+
+	private List<TrainingTypeSummaryDTO> requiredTrainings;
+
 }
diff --git a/src/main/java/nl/tudelft/tam/model/JobOffer.java b/src/main/java/nl/tudelft/tam/model/JobOffer.java
index 3755067f93fbff8cd3d174f84f2ae296eb85e75d..a613c614e172b39ca29491da06b49bb25f530e9c 100644
--- a/src/main/java/nl/tudelft/tam/model/JobOffer.java
+++ b/src/main/java/nl/tudelft/tam/model/JobOffer.java
@@ -19,7 +19,9 @@ package nl.tudelft.tam.model;
 
 import java.time.LocalDate;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import javax.annotation.Nullable;
 import javax.persistence.*;
@@ -99,6 +101,10 @@ public class JobOffer {
 	@OneToMany(mappedBy = "jobOffer", cascade = CascadeType.REMOVE)
 	private List<Application> applications = new ArrayList<>();
 
+	@ManyToMany
+	@Builder.Default
+	private Set<TrainingType> requiredTrainings = new HashSet<>();
+
 	@NotNull
 	@Builder.Default
 	@Column(columnDefinition = "DATE")
diff --git a/src/main/java/nl/tudelft/tam/model/TrainingType.java b/src/main/java/nl/tudelft/tam/model/TrainingType.java
index 0c520af02130ff40ec712f1032f26f2fd11d4630..97f4c809d066d5d068b5d917ea34b26a2d16f3f8 100644
--- a/src/main/java/nl/tudelft/tam/model/TrainingType.java
+++ b/src/main/java/nl/tudelft/tam/model/TrainingType.java
@@ -19,7 +19,9 @@ package nl.tudelft.tam.model;
 
 import java.time.LocalDate;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import javax.persistence.*;
 import javax.validation.constraints.NotNull;
@@ -43,6 +45,10 @@ public class TrainingType {
 	@NotNull
 	private Long programId;
 
+	@NotNull
+	@Builder.Default
+	private Boolean isDefault = false;
+
 	@NotNull
 	@Builder.Default
 	@ToString.Exclude
@@ -50,6 +56,13 @@ public class TrainingType {
 	@OneToMany(mappedBy = "type")
 	private List<TrainingApproval> approvalsOfThisType = new ArrayList<>();
 
+	@NotNull
+	@Builder.Default
+	@ToString.Exclude
+	@EqualsAndHashCode.Exclude
+	@ManyToMany(mappedBy = "requiredTrainings")
+	private Set<JobOffer> requiredFor = new HashSet<>();
+
 	@NotNull
 	@Builder.Default
 	@Column(columnDefinition = "DATE")
diff --git a/src/main/java/nl/tudelft/tam/model/records/FlexDelftExport.java b/src/main/java/nl/tudelft/tam/model/records/FlexDelftExport.java
index 8514e6aa666a9d290c574643501ea9ff91826dc7..35a41c1569da6db7c21fb969a529167c4733ddd6 100644
--- a/src/main/java/nl/tudelft/tam/model/records/FlexDelftExport.java
+++ b/src/main/java/nl/tudelft/tam/model/records/FlexDelftExport.java
@@ -52,6 +52,7 @@ public record FlexDelftExport(PersonSummaryDTO applicant, Profile applicantProfi
 		if (jobOffer != null) {
 			return new String[] {
 					applicant.getDisplayName(),
+					applicant.getUsername(),
 					applicant.getEmail(),
 					jobOffer.getContractName(),
 					jobOffer.getBaanCode(),
diff --git a/src/main/java/nl/tudelft/tam/repository/TrainingApprovalRepository.java b/src/main/java/nl/tudelft/tam/repository/TrainingApprovalRepository.java
index 3c107026a3fc09635a40b83d51ed5b2ddfe567d7..f2de30543c95ba06b15a8922110868cb18fb482e 100644
--- a/src/main/java/nl/tudelft/tam/repository/TrainingApprovalRepository.java
+++ b/src/main/java/nl/tudelft/tam/repository/TrainingApprovalRepository.java
@@ -41,7 +41,7 @@ public interface TrainingApprovalRepository extends JpaRepository<TrainingApprov
 	}
 
 	/**
-	 * Fina training approvals by person id and type (should only ever be empty or 1)
+	 * Finds training approvals by person id and type (should only ever be empty or 1)
 	 *
 	 * @param  personId The person for whom to find the approval
 	 * @param  type     The type of the approval
@@ -49,6 +49,15 @@ public interface TrainingApprovalRepository extends JpaRepository<TrainingApprov
 	 */
 	Optional<TrainingApproval> findByPersonIdAndType(Long personId, TrainingType type);
 
+	/**
+	 * Finds training approvals by person id and type id (should only ever be empty or 1)
+	 *
+	 * @param  personId The person for whom to find the approval
+	 * @param  typeId   The id of the type of the approval
+	 * @return          The list of approvals matching the criteria (should only every be empty or 1)
+	 */
+	Optional<TrainingApproval> findByPersonIdAndTypeId(Long personId, Long typeId);
+
 	/**
 	 * Delete all training approvals by type
 	 *
diff --git a/src/main/java/nl/tudelft/tam/security/AuthorisationService.java b/src/main/java/nl/tudelft/tam/security/AuthorisationService.java
index 5399910d35393adef023011e4304bb8f5ee27387..857c4f6665a013716f8cce52da9b184a4d8d1fe7 100644
--- a/src/main/java/nl/tudelft/tam/security/AuthorisationService.java
+++ b/src/main/java/nl/tudelft/tam/security/AuthorisationService.java
@@ -17,6 +17,7 @@
  */
 package nl.tudelft.tam.security;
 
+import java.time.LocalDate;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -35,6 +36,7 @@ import nl.tudelft.labracore.api.dto.RoleDetailsDTO;
 import nl.tudelft.labracore.lib.security.LabradorUserDetails;
 import nl.tudelft.labracore.lib.security.user.DefaultRole;
 import nl.tudelft.labracore.lib.security.user.Person;
+import nl.tudelft.tam.model.JobOffer;
 import nl.tudelft.tam.service.*;
 
 @Service
@@ -177,8 +179,6 @@ public class AuthorisationService {
 	 */
 	public boolean isManagerOfJob(Long offerId) {
 		Long editionId = jobOfferService.getEditionFromJobOffer(offerId);
-		if (isAdmin() || isCoordinatorForEdition(editionId))
-			return true;
 		return isManagerOfEdition(editionId);
 	}
 
@@ -190,8 +190,6 @@ public class AuthorisationService {
 	 */
 	public boolean isManagerOfWork(Long workId) {
 		Long editionId = extraWorkService.getEditionFromExtraWork(workId);
-		if (isAdmin() || isCoordinatorForEdition(editionId))
-			return true;
 		return isManagerOfEdition(editionId);
 	}
 
@@ -202,12 +200,28 @@ public class AuthorisationService {
 	 * @return           true if teacher or head ta
 	 */
 	public boolean isManagerOfEdition(Long editionId) {
+		if (isAdmin() || isCoordinatorForEdition(editionId)) {
+			return true;
+		}
 		List<RoleDetailsDTO> roles = roleService
 				.getRolesById(List.of(editionId),
 						List.of(getAuthPerson().getId()));
 		return roles.size() == 1 && roles.get(0).getType().equals(RoleDetailsDTO.TypeEnum.TEACHER);
 	}
 
+	/**
+	 * Checks if a user can submit an application for a job offer.
+	 *
+	 * @param  offerId The id of the job offer
+	 * @return         True iff the user can submit a job offer
+	 */
+	public boolean canSubmitApplication(Long offerId) {
+		JobOffer jobOffer = jobOfferService.findByIdOrThrow(offerId);
+		return !applicationService.appExistsFor(getAuthPerson().getId(), offerId) &&
+				!jobOffer.getHidden() &&
+				(jobOffer.getDeadline() == null || !LocalDate.now().isAfter(jobOffer.getDeadline()));
+	}
+
 	/**
 	 * Checks if an applications exists for that job offer for current authed person
 	 *
@@ -241,6 +255,10 @@ public class AuthorisationService {
 	/**
 	 * Checks whether the user is authorised to approve TA training.
 	 *
+	 * Note: When changing this, check the controller for ta training, since the actual filtering by programme
+	 * is done there, this mainly just determines who can make requests to the endpoints and see the page as a
+	 * whole.
+	 *
 	 * @return true if the user is authorised
 	 */
 	public boolean canEditTaTraining() {
@@ -259,6 +277,10 @@ public class AuthorisationService {
 	/**
 	 * Checks whether the user is authorised to edit a RaiseRequest claim.
 	 *
+	 * Note: When changing this, check the controller for raise requests, since the actual filtering by
+	 * programme is done there, this mainly just determines who can make requests to the endpoints and see the
+	 * page as a whole.
+	 *
 	 * @return true if the user is authorised
 	 */
 	public boolean canEditPayScale() {
diff --git a/src/main/java/nl/tudelft/tam/service/ApplicationService.java b/src/main/java/nl/tudelft/tam/service/ApplicationService.java
index 268c17f7b976d07a961d8ff29c122862891748d6..c68eb3796b6a1d60e9ddfb9854f2357bf403fd5d 100644
--- a/src/main/java/nl/tudelft/tam/service/ApplicationService.java
+++ b/src/main/java/nl/tudelft/tam/service/ApplicationService.java
@@ -39,12 +39,15 @@ import nl.tudelft.labracore.lib.security.user.Person;
 import nl.tudelft.tam.dto.create.ApplicationCreateDTO;
 import nl.tudelft.tam.dto.create.EmailCreateDTO;
 import nl.tudelft.tam.dto.id.JobOfferIdDTO;
+import nl.tudelft.tam.dto.util.ApplicationFilterDTO;
 import nl.tudelft.tam.dto.view.details.ApplicationDetailsJobOfferDTO;
 import nl.tudelft.tam.dto.view.details.ApplicationDetailsPersonDTO;
 import nl.tudelft.tam.dto.view.details.JobOfferDetailsDTO;
+import nl.tudelft.tam.dto.view.details.TrainingTypeDetailsDTO;
 import nl.tudelft.tam.dto.view.summary.ApplicationStatusSummaryDTO;
 import nl.tudelft.tam.dto.view.summary.ApplicationSummaryDTO;
 import nl.tudelft.tam.dto.view.summary.JobOfferSummaryDTO;
+import nl.tudelft.tam.dto.view.summary.TrainingTypeSummaryDTO;
 import nl.tudelft.tam.enums.Status;
 import nl.tudelft.tam.exception.ApplicationStatusException;
 import nl.tudelft.tam.model.*;
@@ -359,7 +362,7 @@ public class ApplicationService {
 					"Trying to change application from status:[" + app.getStatus() + "] to accepted.");
 		setStatus(app, Status.ACCEPTED);
 
-		roleService.changeToTa(personId, offerId);
+		roleService.changeToTa(personId, offerId, false);
 	}
 
 	/**
@@ -395,7 +398,7 @@ public class ApplicationService {
 			throw new IllegalStateException(
 					"Trying to change application from status:[" + app.getStatus() + "] to demoted.");
 
-		roleService.changeToTa(personId, offerId);
+		roleService.changeToTa(personId, offerId, true);
 	}
 
 	// endregion
@@ -731,17 +734,39 @@ public class ApplicationService {
 	}
 
 	/**
-	 * Filter a list of applications to only applications changed since a timestamp.
+	 * Gets the list of coordinating applications, filtered by the provided filter.
+	 *
+	 * @param  personId The id of the coordinator
+	 * @param  filter   The filter to apply
+	 * @return          The filtered list of coordinating applications
+	 */
+	public List<Application> getFilteredCoordinatingApplications(Long personId,
+			ApplicationFilterDTO filter) {
+		List<Application> applications = getCoordinatingApplicationsForProgram(personId, filter.getProgram())
+				.stream()
+				.filter(a -> !editionService.getOrThrow(a.getJobOffer().getEditionId()).getIsArchived())
+				.toList();
+		return filterApplications(applications, filter);
+	}
+
+	/**
+	 * Filter a list of applications.
 	 *
 	 * @param  applications The list of applications
-	 * @param  since        The timestamp
+	 * @param  filter       The filter to apply
 	 * @return              The filtered list of applications
 	 */
-	public List<Application> filterApplicationsChangedSince(List<Application> applications,
-			LocalDateTime since) {
+	public List<Application> filterApplications(List<Application> applications,
+			ApplicationFilterDTO filter) {
 		return applications.stream()
-				.filter(app -> !applicationChangeEventRepository
-						.findByApplicationIdAndChangedAtAfter(app.getId(), since).isEmpty())
+				.filter(app -> filter.getStatuses() == null || filter.getStatuses().contains(app.getStatus()))
+				.filter(app -> filter.getContractStartAfter() == null
+						|| !app.getJobOffer().getContractStartDate().isBefore(filter.getContractStartAfter()))
+				.filter(app -> filter.getContractStartBefore() == null
+						|| !app.getJobOffer().getContractStartDate().isAfter(filter.getContractStartBefore()))
+				.filter(app -> filter.getUpdatedSince() == null || !applicationChangeEventRepository
+						.findByApplicationIdAndChangedAtAfter(app.getId(), filter.getUpdatedSince())
+						.isEmpty())
 				.toList();
 	}
 
@@ -780,11 +805,18 @@ public class ApplicationService {
 					"Status", "Student Response" };
 		}
 
-		Map<TrainingType, String> trainingTypes = trainingTypeService.getAllWithProgramNames();
+		List<TrainingTypeDetailsDTO> trainingTypes = trainingTypeService
+				.getDetails(
+						applications.stream().flatMap(a -> a.getJobOffer().getRequiredTrainings().stream())
+								.map(TrainingType::getId).distinct().toList())
+				.stream()
+				.sorted(Comparator.<TrainingTypeDetailsDTO, String>comparing(t -> t.getProgram().getName())
+						.thenComparing(TrainingTypeSummaryDTO::getName))
+				.toList();
 
-		for (Map.Entry<TrainingType, String> pair : trainingTypes.entrySet()) {
-			String trainingString = pair.getValue() + " - " + pair.getKey().getName();
-			headers = ArrayUtils.add(headers, trainingString);
+		for (TrainingTypeDetailsDTO trainingType : trainingTypes) {
+			headers = ArrayUtils.add(headers,
+					trainingType.getProgram().getName() + " - " + trainingType.getName());
 		}
 
 		Map<Long, PersonSummaryDTO> people = personService
@@ -814,13 +846,18 @@ public class ApplicationService {
 			row[i++] = app.getStatus().toString();
 			row[i++] = app.getContent();
 
-			for (Map.Entry<TrainingType, String> pair : trainingTypes.entrySet()) {
-				Optional<TrainingApproval> approval = trainingApprovalService
-						.findByPersonIdAndType(applicant.getId(), pair.getKey());
-				if (approval.isPresent()) {
-					row[i++] = "Completed";
+			for (TrainingTypeDetailsDTO trainingType : trainingTypes) {
+				if (app.getJobOffer().getRequiredTrainings().stream()
+						.anyMatch(t -> Objects.equals(t.getId(), trainingType.getId()))) {
+					Optional<TrainingApproval> approval = trainingApprovalService
+							.findByPersonIdAndTypeId(applicant.getId(), trainingType.getId());
+					if (approval.isPresent()) {
+						row[i++] = "Completed";
+					} else {
+						row[i++] = "Not Completed";
+					}
 				} else {
-					row[i++] = "Not Completed";
+					row[i++] = "N/A";
 				}
 			}
 
@@ -851,7 +888,8 @@ public class ApplicationService {
 			List<Application> applications, LocalDateTime since)
 			throws IOException {
 		if (since != null) {
-			applications = filterApplicationsChangedSince(applications, since);
+			applications = filterApplications(applications,
+					ApplicationFilterDTO.builder().updatedSince(since).build());
 		}
 
 		applicationExportRepository.save(ApplicationExport.builder().exportedBy(person.getId())
@@ -864,7 +902,7 @@ public class ApplicationService {
 
 		String[] headers;
 
-		headers = new String[] { "Name", "Email", "Contract Name", "Baan Code", "Approver Name",
+		headers = new String[] { "Name", "NetID", "Email", "Contract Name", "Baan Code", "Approver Name",
 				"Approver Email",
 				"Coordinator",
 				"Max Hours", "PayScale", "Contract Start Date", "Contract End Date", "Batch", "Remarks" };
diff --git a/src/main/java/nl/tudelft/tam/service/JobOfferService.java b/src/main/java/nl/tudelft/tam/service/JobOfferService.java
index d6cf87ef9b1a00ebb0c98f96350882eb32659ce8..6f9283bcfefc869b854413d1e0e07135addb2521 100644
--- a/src/main/java/nl/tudelft/tam/service/JobOfferService.java
+++ b/src/main/java/nl/tudelft/tam/service/JobOfferService.java
@@ -50,6 +50,7 @@ import nl.tudelft.tam.io.ImportHandler;
 import nl.tudelft.tam.io.JobOfferImportHandler;
 import nl.tudelft.tam.model.Application;
 import nl.tudelft.tam.model.JobOffer;
+import nl.tudelft.tam.model.TrainingType;
 import nl.tudelft.tam.repository.JobOfferRepository;
 
 @Service
@@ -108,6 +109,9 @@ public class JobOfferService {
 	@Autowired
 	JobOfferImportHandler importHandler;
 
+	@Autowired
+	private TrainingTypeService trainingTypeService;
+
 	@Value("${tam.filesys.imports-dir}")
 	private String importDirectory;
 
@@ -167,7 +171,9 @@ public class JobOfferService {
 	 */
 	@Transactional
 	public Long newJobOffer(JobOfferCreateDTO dto) {
-		return jobOfferRepository.save(dto.apply()).getId();
+		JobOffer offer = jobOfferRepository.save(dto.apply());
+		setDefaultTrainings(offer);
+		return offer.getId();
 	}
 
 	/**
@@ -307,6 +313,19 @@ public class JobOfferService {
 				.orElse(null);
 	}
 
+	/**
+	 * Sets the default trainings for a job offer to the programme default.
+	 *
+	 * @param offer The offer to configure
+	 */
+	private void setDefaultTrainings(JobOffer offer) {
+		Long programId = courseService
+				.getCourseById(editionService.getEditionById(offer.getEditionId()).getCourse().getId())
+				.getProgram().getId();
+		offer.setRequiredTrainings(trainingTypeService.getAllTrainingTypesForPrograms(List.of(programId))
+				.stream().filter(TrainingType::getIsDefault).collect(Collectors.toSet()));
+	}
+
 	/**
 	 * Find a job offer by id
 	 *
@@ -354,6 +373,7 @@ public class JobOfferService {
 		List<String[]> lines = csvService.storeAndReadCSV(file, importDirectory);
 
 		var result = importHandler.importFile(lines);
+		result.result().forEach(this::setDefaultTrainings);
 		jobOfferRepository.saveAll(result.result());
 		return result;
 	}
@@ -369,7 +389,9 @@ public class JobOfferService {
 			Long editionId = editionService.addEdition(edition);
 			jobOfferRepository.saveAll(offers.stream().map(offer -> {
 				offer.setEditionId(editionId);
-				return offer.apply();
+				JobOffer created = offer.apply();
+				setDefaultTrainings(created);
+				return created;
 			}).toList());
 		});
 	}
@@ -384,4 +406,30 @@ public class JobOfferService {
 		JobOffer offer = jobOfferRepository.findByIdOrThrow(id);
 		offer.setHidden(!offer.getHidden());
 	}
+
+	/**
+	 * Adds a required training to a job offer.
+	 *
+	 * @param id     The id of the job offer
+	 * @param typeId The id of the training type
+	 */
+	@Transactional
+	public void addRequiredTraining(Long id, Long typeId) {
+		JobOffer offer = jobOfferRepository.findByIdOrThrow(id);
+		offer.getRequiredTrainings().add(trainingTypeService.getTrainingTypeById(typeId));
+		jobOfferRepository.save(offer);
+	}
+
+	/**
+	 * Removes a required training to a job offer.
+	 *
+	 * @param id     The id of the job offer
+	 * @param typeId The id of the training type
+	 */
+	@Transactional
+	public void removeRequiredTraining(Long id, Long typeId) {
+		JobOffer offer = jobOfferRepository.findByIdOrThrow(id);
+		offer.getRequiredTrainings().remove(trainingTypeService.getTrainingTypeById(typeId));
+		jobOfferRepository.save(offer);
+	}
 }
diff --git a/src/main/java/nl/tudelft/tam/service/RoleService.java b/src/main/java/nl/tudelft/tam/service/RoleService.java
index 314f8a8acda765a37e21ba0e5d5a220d5de032b8..fd48f4ee9a34658575b8d3ecd29839c8f0fe02d0 100644
--- a/src/main/java/nl/tudelft/tam/service/RoleService.java
+++ b/src/main/java/nl/tudelft/tam/service/RoleService.java
@@ -137,11 +137,17 @@ public class RoleService {
 	 *
 	 * @param personId The person whose role to change
 	 * @param offerId  The job offer whose edition to change the role in
+	 * @param demote   Whether to demote the person from Head TA to TA
 	 */
-	public void changeToTa(Long personId, Long offerId) {
+	public void changeToTa(Long personId, Long offerId, boolean demote) {
 		Long editionId = jobOfferService.getEditionFromJobOffer(offerId);
 
 		if (hasRole(personId, editionId)) {
+			// ensure student isn't demoted from HeadTA to TA on accepting.
+			if (hasHeadTARole(personId, editionId) && !demote) {
+				return;
+			}
+
 			roleControllerApi
 					.patchRole(new RolePatchDTO().type(RolePatchDTO.TypeEnum.TA), personId, editionId)
 					.block();
diff --git a/src/main/java/nl/tudelft/tam/service/TrainingApprovalService.java b/src/main/java/nl/tudelft/tam/service/TrainingApprovalService.java
index a24612f306d2de3ce8dff141990d7ba2c65908f6..fec1ffb254a79a929c2dbb2f68c927d699bee633 100644
--- a/src/main/java/nl/tudelft/tam/service/TrainingApprovalService.java
+++ b/src/main/java/nl/tudelft/tam/service/TrainingApprovalService.java
@@ -141,6 +141,17 @@ public class TrainingApprovalService {
 		return trainingApprovalRepository.findByPersonIdAndType(personId, type);
 	}
 
+	/**
+	 * Find a training approval by person id and type id
+	 *
+	 * @param  personId The person to find the approval for
+	 * @param  typeId   The training type to find the approval for
+	 * @return          An optional of the training approval
+	 */
+	public Optional<TrainingApproval> findByPersonIdAndTypeId(Long personId, Long typeId) {
+		return trainingApprovalRepository.findByPersonIdAndTypeId(personId, typeId);
+	}
+
 	/**
 	 * Imports a list of training approvals from a CSV file. Format:<code>
 	 *     [0] = assistant id, [1] = training type, [2] = date passed
diff --git a/src/main/java/nl/tudelft/tam/service/TrainingTypeService.java b/src/main/java/nl/tudelft/tam/service/TrainingTypeService.java
index a799090bc9100ba02cc0d258dcc69656f694a2b0..16e4c1f2c1297f86fe1696a1986102d61d42326f 100644
--- a/src/main/java/nl/tudelft/tam/service/TrainingTypeService.java
+++ b/src/main/java/nl/tudelft/tam/service/TrainingTypeService.java
@@ -25,12 +25,16 @@ import java.util.stream.Collectors;
 
 import javax.transaction.Transactional;
 
+import org.modelmapper.ModelMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 
+import nl.tudelft.labracore.api.dto.ProgramDetailsDTO;
+import nl.tudelft.labracore.api.dto.ProgramSummaryDTO;
 import nl.tudelft.tam.dto.create.TrainingTypeCreateDTO;
 import nl.tudelft.tam.dto.patch.TrainingTypePatchDTO;
+import nl.tudelft.tam.dto.view.details.TrainingTypeDetailsDTO;
 import nl.tudelft.tam.model.TrainingType;
 import nl.tudelft.tam.repository.TrainingTypeRepository;
 
@@ -47,6 +51,9 @@ public class TrainingTypeService {
 	@Autowired
 	ProgramService programService;
 
+	@Autowired
+	ModelMapper mapper;
+
 	/**
 	 * Get all training types for programs
 	 *
@@ -132,4 +139,23 @@ public class TrainingTypeService {
 
 		return result;
 	}
+
+	/**
+	 * Get a list of training type details.
+	 *
+	 * @param  ids The ids to retrieve
+	 * @return     The list of training type details
+	 */
+	public List<TrainingTypeDetailsDTO> getDetails(List<Long> ids) {
+		List<TrainingType> trainingTypes = repository.findAllById(ids);
+		Map<Long, ProgramSummaryDTO> programmes = programService
+				.getProgramsById(trainingTypes.stream().map(TrainingType::getProgramId).distinct().toList())
+				.stream().collect(Collectors.toMap(ProgramDetailsDTO::getId,
+						p -> mapper.map(p, ProgramSummaryDTO.class)));
+		return trainingTypes.stream().map(t -> {
+			TrainingTypeDetailsDTO dto = mapper.map(t, TrainingTypeDetailsDTO.class);
+			dto.setProgram(programmes.get(t.getProgramId()));
+			return dto;
+		}).toList();
+	}
 }
diff --git a/src/main/resources/email/html/contract_requests_pending.html b/src/main/resources/email/html/contract_requests_pending.html
new file mode 100644
index 0000000000000000000000000000000000000000..164a1fb6ba90d67d66c0f8782eef2282d77ecc94
--- /dev/null
+++ b/src/main/resources/email/html/contract_requests_pending.html
@@ -0,0 +1,66 @@
+<!--
+
+    TAM
+    Copyright (C) 2021 - 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" style="font-family: sans-serif">
+    <head>
+        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    </head>
+
+    <body>
+        <div>
+            <div style="width: 100%; background-color: #00a8db">
+                <a th:href="#{general.app_url}" style="text-decoration: none">
+                    <span style="color: white; font-size: 40px">TAM</span>
+                </a>
+            </div>
+
+            <div>
+                <div style="padding: 0.5em 0">
+                    <p th:text="#{email.greeting(${name})}"></p>
+
+                    <p
+                        th:text="#{email.extraWork.unhandled.message(${listOfBulletPoints.size()})}"></p>
+                    <ul>
+                        <li
+                            th:each="bulletPoint : ${listOfBulletPoints}"
+                            th:text="${bulletPoint}"></li>
+                    </ul>
+
+                    <p th:text="#{email.extraWork.unhandled.callToAction}"></p>
+
+                    <p th:text="#{email.closing}"></p>
+                    <p th:text="#{email.closing.person}"></p>
+                </div>
+
+                <div>
+                    <p style="color: #707070">
+                        <span th:text="#{email.notificationPreferences}"></span>
+                        <a
+                            style="color: #00a6d6; text-decoration: none"
+                            target="_blank"
+                            th:href="|#{general.app_url}/profile|"
+                            th:text="#{email.notificationPreferences.here}"></a>
+                    </p>
+                </div>
+            </div>
+        </div>
+    </body>
+</html>
diff --git a/src/main/resources/email/html/raise_requests_pending.html b/src/main/resources/email/html/raise_requests_pending.html
new file mode 100644
index 0000000000000000000000000000000000000000..c378ddd3bf8929bd4028b97afc880ffc73111efd
--- /dev/null
+++ b/src/main/resources/email/html/raise_requests_pending.html
@@ -0,0 +1,58 @@
+<!--
+
+    TAM
+    Copyright (C) 2021 - 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" style="font-family: sans-serif">
+    <head>
+        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    </head>
+
+    <body>
+        <div>
+            <div style="width: 100%; background-color: #00a8db">
+                <a th:href="#{general.app_url}" style="text-decoration: none">
+                    <span style="color: white; font-size: 40px">TAM</span>
+                </a>
+            </div>
+
+            <div>
+                <div style="padding: 0.5em 0">
+                    <p th:text="#{email.greeting(${name})}"></p>
+
+                    <p th:text="#{email.raiseRequests.pending.message}"></p>
+
+                    <p th:text="#{email.closing}"></p>
+                    <p th:text="#{email.closing.person}"></p>
+                </div>
+
+                <div>
+                    <p style="color: #707070">
+                        <span th:text="#{email.notificationPreferences}"></span>
+                        <a
+                            style="color: #00a6d6; text-decoration: none"
+                            target="_blank"
+                            th:href="|#{general.app_url}/profile|"
+                            th:text="#{email.notificationPreferences.here}"></a>
+                    </p>
+                </div>
+            </div>
+        </div>
+    </body>
+</html>
diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties
index 36e1d30a199419b3475a939767085b323fba6ca0..e1be6d2c16b83e6d24900112533041b50f56601b 100644
--- a/src/main/resources/messages.properties
+++ b/src/main/resources/messages.properties
@@ -99,7 +99,7 @@ edition.endDate = End Date
 edition.endDate.enter = Enter end date... (dd-mm-yyyy HH:mm)
 edition.endDate.tooltip = Enter a date after the last resit for this edition.
 edition.name.tooltip = Enter only the 'edition' part of the name, e.g. '23/24 Q1'
-edition.empty = No job offers or extra work created.
+edition.empty = No job offers or contract offers created.
 # Edition Enrollability
 edition.enrollability = Enrolment Policy
 edition.enrollability.open = Open
@@ -172,6 +172,8 @@ jobOffer.contractName = Contract Name
 jobOffer.contractName.enter = Enter a name for the FlexDelft contract...
 jobOffer.contractStartDate = Contract Start Date
 jobOffer.contractStartDate.enter = Enter a start date for the FlexDelft contract...
+jobOffer.contractStartDateAfter = Contract start after
+jobOffer.contractStartDateBefore = Contract start before
 jobOffer.contractEndDate = Contract End Date
 jobOffer.contractEndDate.enter = Enter an end date for the FlexDelft contract...
 jobOffer.academicLevel = Academic Level
@@ -308,6 +310,7 @@ jobOffer.academicLevel.OTHER = Other
 
 # Application General
 application.all = All Applications
+application.forFlexDelft = For FlexDelft
 application.my = My Applications
 application.many = Applications
 application.new = Apply
@@ -339,8 +342,8 @@ role.ta = TA
 role.head_ta = Head TA
 
 # Extra Work General
-extraWork = Contract Request
-extraWork.many = Contract Requests
+extraWork = Contract Offer
+extraWork.many = Contract Offers
 extraWork.description = Work Description
 extraWork.remainingTime = Time Remaining
 extraWork.contractName = Contract Name
@@ -365,17 +368,17 @@ extraWork.deadline.none = No Deadline
 extraWork.deadline.closed = Closed
 extraWork.date.enter = Enter a deadline...
 extraWork.description.enter = Enter a description (e.g.: Grading, Oral checks, …)
-extraWork.details = Extra Work Details
-extraWork.empty = No Contract Requests Available
-extraWork.empty.description = There is currently no open extra work. Come back later to see if there is something new.
-extraWork.add = New Extra Work
+extraWork.details = Contract Offer Details
+extraWork.empty = No Contract Offers Available
+extraWork.empty.description = There are currently no open contract offers. Come back later to see if there is something new.
+extraWork.add = New Contract Offer
 # Extra Work Imports
-extraWork.import = Import Extra Work
-extraWork.import.info = Import bulk extra work via a CSV file. The first line is considered as headers and will not be read. Each extra work offer should be a separate row, with the following columns in this order:
+extraWork.import = Import Contract Offers
+extraWork.import.info = Import bulk contract offers via a CSV file. The first line is considered as headers and will not be read. Each contract offer should be a separate row, with the following columns in this order:
 extraWork.import.info.columnOrder = Edition Name, Course Code, Work Name, Contract Name, Contract Baan Code, Max Hours, Contract Request Deadline
 extraWork.import.info.downloadTemplate = Download Template
 # Extra Work Actions
-extraWork.edit = Edit Extra Work
+extraWork.edit = Edit Contract Offer
 extraWork.approve = Approve
 extraWork.approve.confirm = Confirm Approval
 extraWork.approve.confirm.desc = Are you sure you would like to approve the following contract requests:
@@ -383,14 +386,14 @@ extraWork.reject = Reject
 extraWork.reject.confirm = Confirm Rejection
 extraWork.reject.confirm.desc = Are you sure you would like to reject the following contract requests:
 extraWork.closeDeadline = Close New Contract Requests
-extraWork.closeDeadline.desc = Are you sure you want to close new contract requests? No other people will be able to request contracts for this extra work anymore.
+extraWork.closeDeadline.desc = Are you sure you want to close new contract requests? No other people will be able to request contracts for this contract offer anymore.
 extraWork.rejectAll = Reject All Open Contract Requests
 extraWork.rejectAll.desc = Are you sure you want to reject all contract requests? All contract requests that have not been approved yet, will be rejected.
 extraWork.approveAll = Approve All Open Contract Requests
 extraWork.approveAll.desc = Are you sure you want to approve all contract requests? All contract requests that have not been approved or rejected, will be approved.
 extraWork.retract = Retract
 extraWork.retract.confirm = Confirm Retraction
-extraWork.retract.confirm.desc = Are you sure you would like to retract your contract requests for the following extra work:
+extraWork.retract.confirm.desc = Are you sure you would like to retract your contract requests for the following contract offer:
 # Extra Work Exports
 extraWork.export = Export Contract Requests
 extraWork.export.FlexDelft = FlexDelft Export
@@ -403,8 +406,8 @@ declaration.new = Request Contract
 declaration.new.full = New Contract Request
 declaration.empty = Currently no contract requests. Try requesting a contract.
 declaration.empty.staff = Currently no submitted contract requests to review.
-declaration.search = Course / Description / Status …
-declaration.manager.search = Person / Status …
+declaration.search = Course / Description / Status..
+declaration.manager.search = Person / Status..
 declaration.number.submitted = # of contract requests
 # Declaration Hours
 declaration.hours.submitted = total declared time
@@ -588,7 +591,11 @@ email.application.rejected.rejectionMessage = The following message was provided
 email.declaration.rejected.title = Contract Request Rejected
 email.declaration.rejected.message = Sadly, your contract request of {0} hours for {1} in the {2} ({3}) course was rejected. For more information, please contact the course staff.
 email.declaration.approved.title = Contract Request Approved
-email.declaration.approved.message = Your contract request of {0} hours for {1} in the {2} ({3}) course was approved! You will be able to declare these hours in FlexDelft soon. For more information, please contact the course staff.
+email.declaration.approved.message = Your contract request of {0} hours for {1} in the {2} ({3}) course was approved! You will be able to declare these hours in FlexDelft soon. For more information please contact the course staff.
+email.extraWork.unhandled.title = Unhandled Contract Requests
+email.extraWork.unhandled.message = There are currently {0} unhandled contract requests for the following courses:
+email.extraWork.unhandled.callToAction = Please review these contract requests as soon as possible.
+email.raiseRequests.pending.message = There are currently pending raise requests. Please review these as soon as possible.
 
 # Searching
 search.people = Search people
diff --git a/src/main/resources/migrations.yaml b/src/main/resources/migrations.yaml
index 7cd384dd8f13628276042ab31c8b028da043b1b9..ea29f68d12e9d6f98e20d1e5a6f33e4227610cc8 100644
--- a/src/main/resources/migrations.yaml
+++ b/src/main/resources/migrations.yaml
@@ -607,6 +607,91 @@ databaseChangeLog:
           referencedTableName: declaration
           validate: true
 
+# Training improvements
+- changeSet:
+    id: 1711617123011-1
+    author: ruben (generated)
+    changes:
+      - createTable:
+          columns:
+            - column:
+                constraints:
+                  nullable: false
+                  primaryKey: true
+                  primaryKeyName: CONSTRAINT_1A
+                name: required_for_id
+                type: BIGINT
+            - column:
+                constraints:
+                  nullable: false
+                  primaryKey: true
+                  primaryKeyName: CONSTRAINT_1A
+                name: required_trainings_id
+                type: BIGINT
+          tableName: job_offer_required_trainings
+- changeSet:
+    id: 1711617123011-2
+    author: ruben (generated)
+    changes:
+      - addColumn:
+          columns:
+            - column:
+                constraints:
+                  nullable: false
+                name: is_default
+                type: BOOLEAN
+                defaultValueBoolean: false
+          tableName: training_type
+- changeSet:
+    id: 1711617123011-3
+    author: ruben (generated)
+    changes:
+      - createIndex:
+          columns:
+            - column:
+                name: required_trainings_id
+          indexName: FKe814orbdyy10q0qojxn5evx3w_INDEX_1
+          tableName: job_offer_required_trainings
+- changeSet:
+    id: 1711617123011-4
+    author: ruben (generated)
+    changes:
+      - createIndex:
+          columns:
+            - column:
+                name: required_for_id
+          indexName: FKqaaau3bkku2gas8c8280obr9h_INDEX_1
+          tableName: job_offer_required_trainings
+- changeSet:
+    id: 1711617123011-5
+    author: ruben (generated)
+    changes:
+      - addForeignKeyConstraint:
+          baseColumnNames: required_trainings_id
+          baseTableName: job_offer_required_trainings
+          constraintName: FKe814orbdyy10q0qojxn5evx3w
+          deferrable: false
+          initiallyDeferred: false
+          onDelete: RESTRICT
+          onUpdate: RESTRICT
+          referencedColumnNames: id
+          referencedTableName: training_type
+          validate: true
+- changeSet:
+    id: 1711617123011-6
+    author: ruben (generated)
+    changes:
+      - addForeignKeyConstraint:
+          baseColumnNames: required_for_id
+          baseTableName: job_offer_required_trainings
+          constraintName: FKqaaau3bkku2gas8c8280obr9h
+          deferrable: false
+          initiallyDeferred: false
+          onDelete: RESTRICT
+          onUpdate: RESTRICT
+          referencedColumnNames: id
+          referencedTableName: job_offer
+          validate: true
 
 # Adding AcademicLevel and AcademicPeriod
 - changeSet:
diff --git a/src/main/resources/static/js/main.js b/src/main/resources/static/js/main.js
index 2e718bf83c29b1d987865397f45ea785fc64a160..1a79092581c2bdad1fadeed5d4b1b5d4623a3dcb 100644
--- a/src/main/resources/static/js/main.js
+++ b/src/main/resources/static/js/main.js
@@ -358,7 +358,7 @@ function declarationSubmit() {
     } else if (isDurationGreaterThanRemainder(timeEl.value)) {
         document.getElementById("new-declaration-time-remaining-error").classList.remove("hidden");
         timeEl.focus();
-    } else if (reasonEl.value || reasonEl.value.length < 10 || reasonEl.value.length > 140) {
+    } else if (!reasonEl.value || reasonEl.value.length < 10 || reasonEl.value.length > 140) {
         document.getElementById("new-declaration-reason-error").classList.remove("hidden");
         reasonEl.focus();
     } else {
diff --git a/src/main/resources/templates/application/all.html b/src/main/resources/templates/application/all.html
index 18b01a63945ca9e904a71edf92d8e152f245d745..141291557fc9b6404cf3ddf1d78d0f9c873c3ea8 100644
--- a/src/main/resources/templates/application/all.html
+++ b/src/main/resources/templates/application/all.html
@@ -25,53 +25,29 @@
     layout:decorate="~{container}">
     <head>
         <title th:text="#{application.all}"></title>
-
-        <script th:inline="javascript">
-            /*<![CDATA[*/
-            let since =
-                /*[[${exports.isEmpty()} ? '' : ${#temporals.format(exports[0].exportAt, 'yyyy-MM-dd''T''HH:mm:ss')}]]*/ "";
-            /*]]>*/
-            let url = new URL(window.location);
-            if (url.searchParams.get("since") === null) {
-                url.searchParams.set("since", since);
-                window.location = url;
-            }
-        </script>
     </head>
 
     <body>
         <div layout:fragment="content" class="flex vertical">
             <h1 class="font-800" th:text="#{application.all}"></h1>
 
+            <nav class="tabs" role="tablist">
+                <a
+                    role="tab"
+                    aria-selected="true"
+                    th:href="@{/application/all}"
+                    th:text="#{application.all}"></a>
+                <a
+                    role="tab"
+                    th:href="@{/application/all/flexdelft}"
+                    th:text="#{application.forFlexDelft}"></a>
+            </nav>
+
             <form
-                id="flex-export-form"
+                id="export-form"
                 class="flex align-end wrap | sm:vertical sm:gap-3 sm:align-stretch"
-                th:action="@{/application/flex-delft-export}"
+                th:action="@{/application/export}"
                 method="get">
-                <div class="flex vertical gap-1">
-                    <label
-                        for="export-updated-since"
-                        th:text="#{jobOffer.export.updatedSince}"></label>
-                    <select class="textfield" id="export-updated-since" name="since">
-                        <option
-                            th:data-batch="${exports[0].batchNumber}"
-                            th:value="${#temporals.format(exports[0].exportAt, 'yyyy-MM-dd''T''HH:mm:ss')}"
-                            th:unless="${exports.isEmpty()}"
-                            th:text="#{jobOffer.export.previousAt(${#temporals.format(exports[0].exportAt, 'HH:mm')}, ${#temporals.format(exports[0].exportAt, 'dd-MM-yyyy')})}"></option>
-                        <option
-                            th:selected="${param.since != null} and ${param.since.toString() == #temporals.format(export.exportAt, 'yyyy-MM-dd''T''HH:mm:ss')}"
-                            th:data-batch="${export.batchNumber}"
-                            th:value="${#temporals.format(export.exportAt, 'yyyy-MM-dd''T''HH:mm:ss')}"
-                            th:each="export, iter : ${exports}"
-                            th:unless="${iter.index == 0}"
-                            th:text="#{jobOffer.export.at(${#temporals.format(export.exportAt, 'HH:mm')}, ${#temporals.format(export.exportAt, 'dd-MM-yyyy')})}"></option>
-                        <option
-                            th:selected="${param.since != null} and ${param.since.toString() == ''}"
-                            th:data-batch="0"
-                            th:value="${null}"
-                            th:text="#{jobOffer.export.beforeFirstExport}"></option>
-                    </select>
-                </div>
                 <div class="flex vertical gap-1">
                     <label for="export-program" th:text="#{jobOffer.export.program}"></label>
                     <select class="textfield" id="export-program" name="program" required>
@@ -84,105 +60,114 @@
                 </div>
                 <div class="flex vertical gap-1">
                     <label
-                        for="export-batch-number"
-                        th:text="#{jobOffer.export.batchNumber}"></label>
+                        for="contract-start-after"
+                        th:text="#{jobOffer.contractStartDateAfter}"></label>
                     <input
                         class="textfield"
-                        id="export-batch-number"
-                        name="batch"
-                        type="number"
-                        min="0"
-                        style="max-width: 6rem"
-                        required />
+                        type="date"
+                        id="contract-start-after"
+                        name="contractStartAfter"
+                        th:value="${param.contractStartAfter}" />
                 </div>
-                <div class="flex gap-3 | sm:grid sm:col-2">
-                    <button
-                        type="submit"
-                        class="button"
-                        th:aria-label="#{jobOffer.export.FlexDelft}"
-                        th:text="#{jobOffer.export.FlexDelft}"></button>
-                    <a
-                        class="button"
-                        data-style="outlined"
-                        th:aria-label="#{jobOffer.export}"
-                        th:href="@{/application/export}"
-                        th:text="#{jobOffer.export}"></a>
+                <div class="flex vertical gap-1">
+                    <label
+                        for="contract-start-before"
+                        th:text="#{jobOffer.contractStartDateBefore}"></label>
+                    <input
+                        class="textfield"
+                        type="date"
+                        id="contract-start-before"
+                        name="contractStartBefore"
+                        th:min="${param.contractStartAfter}"
+                        th:value="${param.contractStartBefore}" />
+                </div>
+                <div class="flex vertical gap-1">
+                    <label for="statuses" th:text="#{jobOffer.status}"></label>
+                    <select data-select id="statuses" name="statuses" multiple>
+                        <option
+                            th:each="status : ${T(nl.tudelft.tam.enums.Status).values()}"
+                            th:selected="${param.statuses != null && param.statuses.length > 0 && param.statuses.contains(status.name())}"
+                            th:value="${status}"
+                            th:text="#{|status.jobOffer.${status.name().toLowerCase()}|}"></option>
+                    </select>
                 </div>
+                <button
+                    type="submit"
+                    class="button"
+                    th:aria-label="#{jobOffer.export}"
+                    th:text="#{jobOffer.export}"></button>
             </form>
 
-            <table class="table" data-style="surface">
-                <tr class="table__header">
-                    <th th:text="#{person.name}"></th>
-                    <th th:text="#{person.number}"></th>
-                    <th th:text="#{edition}"></th>
-                    <th th:text="#{jobOffer.before}"></th>
-                    <th th:text="#{jobOffer}"></th>
-                    <th th:text="#{jobOffer.status}"></th>
-                    <th></th>
-                </tr>
-                <tr th:each="app : ${applications.getContent()}">
-                    <td>
-                        <a
-                            class="link"
-                            target="_blank"
-                            th:href="@{|/profile/${app.id.personId}|}"
-                            th:text="${@personCacheManager.getOrThrow(app.id.personId).displayName}"></a>
-                    </td>
-                    <td th:text="${@personCacheManager.getOrThrow(app.id.personId).number}"></td>
-                    <td
-                        th:with="edition = ${@editionCacheManager.getOrThrow(app.jobOffer.editionId)}"
-                        th:text="|${edition.course.name} - ${edition.name}|"></td>
-                    <td
-                        th:text="${@applicationService.hasTAedBefore(app.id.personId, app.jobOffer.id) ? #messages.msg('general.yes') : #messages.msg('general.no')}"></td>
-                    <td th:text="${app.jobOffer.name}"></td>
-                    <td th:text="#{|status.jobOffer.${#strings.toLowerCase(app.status)}|}"></td>
-                    <td></td>
-                </tr>
-                <tr th:if="${applications.getTotalElements() == 0}">
-                    <td data-empty="true" colspan="6" th:text="#{application.empty.staff}"></td>
-                </tr>
-            </table>
-
-            <th:block
-                layout:replace="~{pagination :: pagination(page=${applications}, size=5)}"></th:block>
+            <th:block layout:replace="~{application/table :: table}"></th:block>
 
             <script>
-                document.addEventListener("DOMContentLoaded", function () {
-                    const updatedSince = $("#export-updated-since");
-
-                    $("#flex-export-form").submit(function (event) {
-                        event.preventDefault();
-                        const program = $("#export-program").val();
-                        const batch = $("#export-batch-number").val();
-                        window.open(
-                            `/application/flex-delft-export?program=${program}&since=${updatedSince.val()}&batch=${batch}`,
-                            "_blank",
-                        );
+                document.addEventListener("ComponentsLoaded", function () {
+                    const program = document.getElementById("export-program");
+
+                    program.addEventListener("change", function () {
                         let url = new URL(window.location);
-                        url.searchParams.set("since", moment().format("yyyy-MM-DDTHH:mm:ss"));
-                        window.location = url;
+                        url.searchParams.set("program", program.value);
+                        window.location.href = url.href;
                     });
-                    $("#export-program").change(function () {
+
+                    const contractStartAfter = document.getElementById("contract-start-after");
+                    const contractStartBefore = document.getElementById("contract-start-before");
+
+                    contractStartAfter.addEventListener("change", function () {
+                        let url = new URL(window.location);
+                        url.searchParams.set("contractStartAfter", contractStartAfter.value);
+                        if (
+                            contractStartBefore.value !== "" &&
+                            moment(contractStartBefore.value).isBefore(
+                                moment(contractStartAfter.value),
+                            )
+                        ) {
+                            url.searchParams.delete("contractStartBefore");
+                        }
+                        window.location.href = url.href;
+                    });
+
+                    contractStartBefore.addEventListener("change", function () {
+                        if (
+                            contractStartAfter.value !== "" &&
+                            moment(contractStartBefore.value).isBefore(
+                                moment(contractStartAfter.value),
+                            )
+                        ) {
+                            return;
+                        }
                         let url = new URL(window.location);
-                        url.searchParams.set("program", $(this).val());
-                        url.searchParams.delete("since");
+                        url.searchParams.set("contractStartBefore", contractStartBefore.value);
                         window.location.href = url.href;
                     });
-                    updatedSince
-                        .change(function () {
-                            $("#export-batch-number").val(
-                                $(this).children(":selected").data("batch") + 1,
-                            );
-                        })
-                        .change(function () {
-                            let url = new URL(window.location);
-                            url.searchParams.set("since", $(this).val());
-                            window.location.href = url.href;
-                        });
-
-                    $("#export-batch-number").val(
-                        updatedSince.children(":selected").data("batch") + 1,
+
+                    const statuses = document.getElementById("statuses");
+
+                    const oldStatuses = [...statuses.querySelectorAll("option[selected]")].map(
+                        opt => opt.value,
                     );
+                    statuses.addEventListener("close", function () {
+                        const selected = [...statuses.querySelectorAll("option[selected]")].map(
+                            opt => opt.value,
+                        );
+
+                        let equal = true;
+                        if (selected.length === oldStatuses.length) {
+                            for (let i = 0; i < selected.length; i++) {
+                                if (selected[i] !== oldStatuses[i]) {
+                                    equal = false;
+                                }
+                            }
+                        }
+                        if (!equal) {
+                            return;
+                        }
+
+                        let url = new URL(window.location);
+                        url.searchParams.delete("statuses");
+                        selected.forEach(status => url.searchParams.append("statuses", status));
+                        window.location.href = url.href;
+                    });
                 });
             </script>
         </div>
diff --git a/src/main/resources/templates/application/all_flexdelft.html b/src/main/resources/templates/application/all_flexdelft.html
new file mode 100644
index 0000000000000000000000000000000000000000..30f6ccad43c1c7d55bb52acee00e97e5bb3ab98a
--- /dev/null
+++ b/src/main/resources/templates/application/all_flexdelft.html
@@ -0,0 +1,162 @@
+<!--
+
+    TAM
+    Copyright (C) 2021 - 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"
+    layout:decorate="~{container}">
+    <head>
+        <title th:text="#{application.all}"></title>
+
+        <script th:inline="javascript">
+            /*<![CDATA[*/
+            let since =
+                /*[[${exports.isEmpty()} ? '' : ${#temporals.format(exports[0].exportAt, 'yyyy-MM-dd''T''HH:mm:ss')}]]*/ "";
+            /*]]>*/
+            let url = new URL(window.location);
+            if (url.searchParams.get("updatedSince") === null) {
+                url.searchParams.set("updatedSince", since);
+                window.location = url;
+            }
+        </script>
+    </head>
+
+    <body>
+        <div layout:fragment="content" class="flex vertical">
+            <h1 class="font-800" th:text="#{application.all}"></h1>
+
+            <nav class="tabs" role="tablist">
+                <a role="tab" th:href="@{/application/all}" th:text="#{application.all}"></a>
+                <a
+                    role="tab"
+                    aria-selected="true"
+                    th:href="@{/application/all/flexdelft}"
+                    th:text="#{application.forFlexDelft}"></a>
+            </nav>
+
+            <form
+                id="flex-export-form"
+                class="flex align-end wrap | sm:vertical sm:gap-3 sm:align-stretch"
+                th:action="@{/application/flex-delft-export}"
+                method="get">
+                <div class="flex vertical gap-1">
+                    <label
+                        for="export-updated-since"
+                        th:text="#{jobOffer.export.updatedSince}"></label>
+                    <select class="textfield" id="export-updated-since" name="updatedSince">
+                        <option
+                            th:data-batch="${exports[0].batchNumber}"
+                            th:value="${#temporals.format(exports[0].exportAt, 'yyyy-MM-dd''T''HH:mm:ss')}"
+                            th:unless="${exports.isEmpty()}"
+                            th:text="#{jobOffer.export.previousAt(${#temporals.format(exports[0].exportAt, 'HH:mm')}, ${#temporals.format(exports[0].exportAt, 'dd-MM-yyyy')})}"></option>
+                        <option
+                            th:selected="${param.updatedSince != null} and ${param.updatedSince.toString() == #temporals.format(export.exportAt, 'yyyy-MM-dd''T''HH:mm:ss')}"
+                            th:data-batch="${export.batchNumber}"
+                            th:value="${#temporals.format(export.exportAt, 'yyyy-MM-dd''T''HH:mm:ss')}"
+                            th:each="export, iter : ${exports}"
+                            th:unless="${iter.index == 0}"
+                            th:text="#{jobOffer.export.at(${#temporals.format(export.exportAt, 'HH:mm')}, ${#temporals.format(export.exportAt, 'dd-MM-yyyy')})}"></option>
+                        <option
+                            th:selected="${param.updatedSince != null} and ${param.updatedSince.toString() == ''}"
+                            th:data-batch="0"
+                            th:value="${null}"
+                            th:text="#{jobOffer.export.beforeFirstExport}"></option>
+                    </select>
+                </div>
+                <div class="flex vertical gap-1">
+                    <label for="export-program" th:text="#{jobOffer.export.program}"></label>
+                    <select class="textfield" id="export-program" name="program" required>
+                        <option
+                            th:each="program : ${programs}"
+                            th:selected="${program.id.toString() == param.program?.toString()}"
+                            th:value="${program.id}"
+                            th:text="${program.name}"></option>
+                    </select>
+                </div>
+                <div class="flex vertical gap-1">
+                    <label
+                        for="export-batch-number"
+                        th:text="#{jobOffer.export.batchNumber}"></label>
+                    <input
+                        class="textfield"
+                        id="export-batch-number"
+                        name="batch"
+                        type="number"
+                        min="0"
+                        style="max-width: 6rem"
+                        required />
+                </div>
+                <button
+                    type="submit"
+                    class="button"
+                    name="type"
+                    value="flexdelft"
+                    th:aria-label="#{jobOffer.export.FlexDelft}"
+                    th:text="#{jobOffer.export.FlexDelft}"></button>
+            </form>
+
+            <th:block th:replace="~{application/table :: table}"></th:block>
+
+            <script>
+                document.addEventListener("DOMContentLoaded", function () {
+                    const updatedSince = $("#export-updated-since");
+
+                    $("#flex-export-form").submit(function (event) {
+                        event.preventDefault();
+                        const program = $("#export-program").val();
+                        const batch = $("#export-batch-number").val();
+                        window.open(
+                            `/application/flex-delft-export?program=${program}&updatedSince=${updatedSince.val()}&batch=${batch}`,
+                            "_blank",
+                        );
+                        let url = new URL(window.location);
+                        url.searchParams.set(
+                            "updatedSince",
+                            moment().format("yyyy-MM-DDTHH:mm:ss"),
+                        );
+                        window.location = url;
+                    });
+                    $("#export-program").change(function () {
+                        let url = new URL(window.location);
+                        url.searchParams.set("program", $(this).val());
+                        url.searchParams.delete("updatedSince");
+                        window.location.href = url.href;
+                    });
+                    updatedSince
+                        .change(function () {
+                            $("#export-batch-number").val(
+                                $(this).children(":selected").data("batch") + 1,
+                            );
+                        })
+                        .change(function () {
+                            let url = new URL(window.location);
+                            url.searchParams.set("updatedSince", $(this).val());
+                            window.location.href = url.href;
+                        });
+
+                    $("#export-batch-number").val(
+                        updatedSince.children(":selected").data("batch") + 1,
+                    );
+                });
+            </script>
+        </div>
+    </body>
+</html>
diff --git a/src/main/resources/templates/application/table.html b/src/main/resources/templates/application/table.html
new file mode 100644
index 0000000000000000000000000000000000000000..c711c425805a51f36f9be2d4062b835b3ee1fa4a
--- /dev/null
+++ b/src/main/resources/templates/application/table.html
@@ -0,0 +1,64 @@
+<!--
+
+    TAM
+    Copyright (C) 2021 - 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>
+        <div id="applications-table" layout:fragment="table">
+            <table class="table mb-5" data-style="surface">
+                <tr class="table__header">
+                    <th th:text="#{person.name}"></th>
+                    <th th:text="#{person.number}"></th>
+                    <th th:text="#{edition}"></th>
+                    <th th:text="#{jobOffer.before}"></th>
+                    <th th:text="#{jobOffer}"></th>
+                    <th th:text="#{jobOffer.status}"></th>
+                    <th></th>
+                </tr>
+                <tr th:each="app : ${applications.getContent()}">
+                    <td>
+                        <a
+                            class="link"
+                            target="_blank"
+                            th:href="@{|/profile/${app.id.personId}|}"
+                            th:text="${@personCacheManager.getOrThrow(app.id.personId).displayName}"></a>
+                    </td>
+                    <td th:text="${@personCacheManager.getOrThrow(app.id.personId).number}"></td>
+                    <td
+                        th:with="edition = ${@editionCacheManager.getOrThrow(app.jobOffer.editionId)}"
+                        th:text="|${edition.course.name} - ${edition.name}|"></td>
+                    <td
+                        th:text="${@applicationService.hasTAedBefore(app.id.personId, app.jobOffer.id) ? #messages.msg('general.yes') : #messages.msg('general.no')}"></td>
+                    <td th:text="${app.jobOffer.name}"></td>
+                    <td th:text="#{|status.jobOffer.${#strings.toLowerCase(app.status)}|}"></td>
+                    <td></td>
+                </tr>
+                <tr th:if="${applications.getTotalElements() == 0}">
+                    <td data-empty="true" colspan="6" th:text="#{application.empty.staff}"></td>
+                </tr>
+            </table>
+
+            <th:block
+                layout:replace="~{pagination :: pagination(page=${applications}, size=5)}"></th:block>
+        </div>
+    </body>
+</html>
diff --git a/src/main/resources/templates/container.html b/src/main/resources/templates/container.html
index b7f7a88eade41c786060a7be0712ac4d6c428f77..438d90986279908acfcac75cdfd9dd9403062107 100644
--- a/src/main/resources/templates/container.html
+++ b/src/main/resources/templates/container.html
@@ -30,7 +30,7 @@
             </header>
             <div class="content-wrapper">
                 <div class="content">
-                    <main layout:fragment="content" th:text="#{general.loadingPlaceholder}"></main>
+                    <main layout:fragment="content">Placeholder</main>
                 </div>
             </div>
             <footer class="footer-wrapper">
diff --git a/src/main/resources/templates/coordinator/create_training_type.html b/src/main/resources/templates/coordinator/create_training_type.html
index 629a0bd4a506c7f6d504581f6c32507ef864abc5..63bbd72fecf1c3c443439090dbb799945a0ffdab 100644
--- a/src/main/resources/templates/coordinator/create_training_type.html
+++ b/src/main/resources/templates/coordinator/create_training_type.html
@@ -46,6 +46,13 @@
                         class="textfield"
                         type="text"
                         required />
+
+                    <div style="grid-column: span 2">
+                        <input id="default" name="isDefault" type="checkbox" value="true" />
+                        <label for="default">
+                            This training is required for jobs of this programme
+                        </label>
+                    </div>
                 </div>
 
                 <div class="flex space-between">
diff --git a/src/main/resources/templates/coordinator/edit_training_type.html b/src/main/resources/templates/coordinator/edit_training_type.html
index cef155ea0837d0b05322110c46a0de9e5301e7b9..506db60c7c9bfcb0917f5434956a9917e122e94f 100644
--- a/src/main/resources/templates/coordinator/edit_training_type.html
+++ b/src/main/resources/templates/coordinator/edit_training_type.html
@@ -25,48 +25,57 @@
     <body>
         <dialog
             th:fragment="overlay"
-            th:id="|edit-training-type-${type.getId()}-overlay|"
+            th:id="|edit-training-type-${type.id}-overlay|"
             class="dialog">
             <div class="flex vertical p-7">
+                <h1 class="underlined font-500" th:text="#{training.type.edit}"></h1>
+
                 <form
-                    th:action="@{|/training-type/update/${programId}/${type.getId()}|}"
+                    th:action="@{/training-type/update/{programId}/{typeId}(programId=${programId}, typeId=${type.id})}"
                     th:method="patch"
-                    th:id="|edit-training-type-${type.getId()}-form|"
-                    hidden></form>
-
-                <h1 class="underlined font-500" th:text="#{training.type.edit}"></h1>
+                    class="flex vertical">
+                    <div class="grid col-2 align-center" style="--col-1: minmax(0, 8rem)">
+                        <label
+                            for="name"
+                            class="center-label"
+                            th:text="#{training.type.name.label}"></label>
+                        <input
+                            id="name"
+                            th:name="name"
+                            th:placeholder="#{training.type.name.enter}"
+                            th:value="${type.name}"
+                            class="textfield"
+                            type="text"
+                            required />
 
-                <div class="grid col-2 align-center" style="--col-1: minmax(0, 8rem)">
-                    <label
-                        for="name"
-                        class="center-label"
-                        th:text="#{training.type.name.label}"></label>
-                    <input
-                        id="name"
-                        th:name="name"
-                        th:placeholder="#{training.type.name.enter}"
-                        th:value="${type.getName()}"
-                        th:form="|edit-training-type-${type.getId()}-form|"
-                        class="textfield"
-                        type="text"
-                        required />
-                </div>
+                        <div style="grid-column: span 2">
+                            <input
+                                th:id="|default-${type.id}|"
+                                name="isDefault"
+                                type="checkbox"
+                                value="true"
+                                th:checked="${type.isDefault}" />
+                            <label th:for="|default-${type.id}|">
+                                This training is required for jobs of this programme
+                            </label>
+                        </div>
+                    </div>
 
-                <div class="flex space-between">
-                    <button
-                        type="button"
-                        class="button p-less"
-                        data-style="outlined"
-                        th:aria-label="#{general.cancel}"
-                        th:text="#{general.cancel}"
-                        data-cancel></button>
-                    <button
-                        type="submit"
-                        class="button p-less"
-                        th:aria-label="#{general.save}"
-                        th:text="#{general.save}"
-                        th:form="|edit-training-type-${type.getId()}-form|"></button>
-                </div>
+                    <div class="flex space-between">
+                        <button
+                            type="button"
+                            class="button p-less"
+                            data-style="outlined"
+                            th:aria-label="#{general.cancel}"
+                            th:text="#{general.cancel}"
+                            data-cancel></button>
+                        <button
+                            type="submit"
+                            class="button p-less"
+                            th:aria-label="#{general.save}"
+                            th:text="#{general.save}"></button>
+                    </div>
+                </form>
             </div>
         </dialog>
     </body>
diff --git a/src/main/resources/templates/coordinator/training.html b/src/main/resources/templates/coordinator/training.html
index e6d50df2792e28bf2c4c418d851c7b8b4253ef6d..3af457e99566b075843bf9b9e8437d3f4a4b88dd 100644
--- a/src/main/resources/templates/coordinator/training.html
+++ b/src/main/resources/templates/coordinator/training.html
@@ -82,7 +82,10 @@
                     </th>
                 </tr>
                 <tr th:each="type : ${trainingTypes}">
-                    <td th:text="${type.name}"></td>
+                    <td>
+                        <span th:text="${type.name}"></span>
+                        <span th:if="${type.isDefault}">(Required)</span>
+                    </td>
                     <td>
                         <div class="flex justify-end gap-3">
                             <button
@@ -100,14 +103,18 @@
                                 th:text="#{general.remove}"></button>
                         </div>
                     </td>
-                    <div th:replace="~{coordinator/confirm_training_type_removal :: overlay}"></div>
-                    <div th:replace="~{coordinator/edit_training_type :: overlay}"></div>
                 </tr>
                 <tr th:if="${trainingTypes.isEmpty()}">
                     <td data-empty="true" colspan="2" th:text="#{coordinator.training.empty}"></td>
                 </tr>
             </table>
 
+            <th:block th:each="type : ${trainingTypes}">
+                <th:block
+                    th:replace="~{coordinator/confirm_training_type_removal :: overlay}"></th:block>
+                <th:block th:replace="~{coordinator/edit_training_type :: overlay}"></th:block>
+            </th:block>
+
             <div th:replace="~{coordinator/create_training_type :: overlay}"></div>
             <div th:replace="~{training_approval/import :: overlay}"></div>
             <div th:replace="~{training_approval/import_csv :: overlay}"></div>
diff --git a/src/main/resources/templates/extra_work/view_one.html b/src/main/resources/templates/extra_work/view_one.html
index 745d53ff324c45fb7e5fb3cb648d4f54d363897c..e64c95afec95dbc4fdf2e9f1d14a5181007e1c72 100644
--- a/src/main/resources/templates/extra_work/view_one.html
+++ b/src/main/resources/templates/extra_work/view_one.html
@@ -24,7 +24,7 @@
     xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
     layout:decorate="~{container}">
     <head>
-        <title th:text="#{extraWork.many}"></title>
+        <title th:text="#{extraWork}"></title>
 
         <style>
             [data-status="offered"] {
diff --git a/src/main/resources/templates/job_offer/view_one.html b/src/main/resources/templates/job_offer/view_one.html
index 87c08884b340414849b57314c55cff4493af6483..cdb95878881b624e3e67611af420a4bb91ec8367 100644
--- a/src/main/resources/templates/job_offer/view_one.html
+++ b/src/main/resources/templates/job_offer/view_one.html
@@ -30,9 +30,11 @@
             [data-status="offered"] {
                 background-color: rgba(255, 212, 0, 0.2);
             }
+
             [data-status^="rejected"] {
                 background-color: rgba(95, 13, 13, 0.2);
             }
+
             [data-status="accepted"] {
                 background-color: rgba(40, 160, 0, 0.2);
             }
@@ -184,15 +186,14 @@
                         th:method="patch"
                         th:action="@{/job-offer/{id}/messages(id=${offer.id})}">
                         <textarea
-                            class="textfield"
-                            data-style="variant"
+                            style="display: none"
+                            data-editor="markdown"
+                            data-line-numbers="10"
                             th:placeholder="#{jobOffer.description.enter}"
                             th:text="${offer.description}"
                             rows="4"
                             name="description"
-                            th:aria-label="${offer.description}"
-                            id="description"
-                            required></textarea>
+                            th:aria-label="${offer.description}"></textarea>
                         <div>
                             <input
                                 id="new-job-offer-require-response"
@@ -310,8 +311,9 @@
                         th:method="patch"
                         th:action="@{/job-offer/{id}/messages(id=${offer.id})}">
                         <textarea
-                            class="textfield"
-                            data-style="variant"
+                            style="display: none"
+                            data-editor="markdown"
+                            data-line-numbers="10"
                             name="hiringMessage"
                             th:placeholder="#{jobOffer.hiringMessage.enter}"
                             th:text="${offer.hiringMessage}"
@@ -331,8 +333,9 @@
                         th:method="patch"
                         th:action="@{/job-offer/{id}/messages(id=${offer.id})}">
                         <textarea
-                            class="textfield"
-                            data-style="variant"
+                            style="display: none"
+                            data-editor="markdown"
+                            data-line-numbers="10"
                             name="rejectMessage"
                             th:placeholder="#{jobOffer.rejectMessage.enter}"
                             th:text="${offer.rejectMessage}"
@@ -347,6 +350,135 @@
                 </div>
             </div>
 
+            <div class="surface flex vertical">
+                <h2 class="underlined font-500">Required trainings</h2>
+                <div
+                    id="add-required-training"
+                    class="flex gap-3"
+                    th:with="availableTrainings = ${trainingTypes.?[!#root.offer.requiredTrainings.![id].contains(#this.id)]}"
+                    th:classappend="${availableTrainings.isEmpty() ? 'hidden' : ''}">
+                    <select
+                        id="available-trainings"
+                        aria-label="Add a required training"
+                        class="textfield"
+                        data-style="variant">
+                        <option
+                            th:each="type : ${availableTrainings}"
+                            th:value="${type.id}"
+                            th:text="${type.name}"></option>
+                    </select>
+                    <button
+                        id="add-required-training-button"
+                        type="button"
+                        class="button p-less"
+                        data-style="outlined">
+                        Add
+                    </button>
+                </div>
+                <ul id="required-trainings" class="list divided" role="list">
+                    <li
+                        th:each="type : ${offer.requiredTrainings}"
+                        th:id="|required-training-${type.id}|"
+                        class="flex align-center gap-5 p-3">
+                        <span th:text="${type.name}"></span>
+                        <button
+                            class="button p-min"
+                            data-style="outlined"
+                            data-type="error"
+                            th:data-remove-training="${type.id}">
+                            Remove
+                        </button>
+                    </li>
+                </ul>
+
+                <script th:inline="javascript">
+                    /*<![CDATA[*/
+                    const offerId = /*[[${offer.id}]]*/ 0;
+                    /*]]>*/
+
+                    document.addEventListener("DOMContentLoaded", function () {
+                        const addTraining = document.getElementById("add-required-training");
+                        const availableTrainings = document.getElementById("available-trainings");
+                        const requiredTrainings = document.getElementById("required-trainings");
+
+                        const token = $("meta[name='_csrf']").attr("content");
+                        const header = $("meta[name='_csrf_header']").attr("content");
+                        let headers = {};
+                        headers[header] = token;
+
+                        function removeTraining(trainingId) {
+                            fetch(`/job-offer/${offerId}/required-training/${trainingId}`, {
+                                method: "DELETE",
+                                headers: headers,
+                            })
+                                .then(res => res.text())
+                                .then(() => {
+                                    const item = document.getElementById(
+                                        `required-training-${trainingId}`,
+                                    );
+
+                                    const option = document.createElement("option");
+                                    option.setAttribute("value", trainingId);
+                                    option.innerText = item.querySelector("span").innerText;
+
+                                    availableTrainings.append(option);
+                                    item.remove();
+
+                                    addTraining.classList.remove("hidden");
+                                });
+                        }
+
+                        document
+                            .querySelectorAll("[data-remove-training]")
+                            .forEach(button =>
+                                button.addEventListener("click", () =>
+                                    removeTraining(button.dataset.removeTraining),
+                                ),
+                            );
+
+                        document
+                            .getElementById("add-required-training-button")
+                            .addEventListener("click", function () {
+                                const trainingId = availableTrainings.value;
+
+                                fetch(`/job-offer/${offerId}/required-training/${trainingId}`, {
+                                    method: "POST",
+                                    headers: headers,
+                                })
+                                    .then(res => res.text())
+                                    .then(() => {
+                                        const span = document.createElement("span");
+                                        span.innerText =
+                                            availableTrainings.selectedOptions[0].innerText;
+
+                                        const removeButton = document.createElement("button");
+                                        removeButton.classList.add("button", "p-min");
+                                        removeButton.setAttribute("data-style", "outlined");
+                                        removeButton.setAttribute("data-type", "error");
+                                        removeButton.innerText = "Remove";
+                                        removeButton.addEventListener("click", () =>
+                                            removeTraining(trainingId),
+                                        );
+
+                                        const item = document.createElement("li");
+                                        item.id = `required-training-${trainingId}`;
+                                        item.classList.add("flex", "align-center", "gap-5", "p-3");
+                                        item.setAttribute("data-id", trainingId);
+                                        item.appendChild(span);
+                                        item.appendChild(removeButton);
+
+                                        requiredTrainings.appendChild(item);
+                                        availableTrainings.selectedOptions[0].remove();
+
+                                        if (availableTrainings.options.length === 0) {
+                                            addTraining.classList.add("hidden");
+                                        }
+                                    });
+                            });
+                    });
+                </script>
+            </div>
+
             <div class="surface flex space-around">
                 <div class="flex vertical gap-0 align-center">
                     <span class="font-700" th:text="${stats[0]}"></span>
diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html
index cd50645be445183368451e009d065748d7240831..9d613c954fec26beb4863fbe506e286fdac2d829 100644
--- a/src/main/resources/templates/layout.html
+++ b/src/main/resources/templates/layout.html
@@ -24,13 +24,11 @@
     xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
     <head>
         <link rel="stylesheet" href="/webjars/font-awesome/css/all.min.css" />
-        <link rel="stylesheet" href="/webjars/chihuahui/1.0.0/main.css" />
+        <link rel="stylesheet" href="/webjars/chihuahui/main.css" />
 
         <script src="/webjars/jquery/jquery.min.js"></script>
-        <script src="/webjars/momentjs/2.29.4/min/moment.min.js"></script>
-        <script type="module" src="/webjars/chihuahui/1.0.0/components.js"></script>
-        <script src="/webjars/chihuahui/1.0.0/selectbox.js"></script>
-        <script src="/webjars/chihuahui/1.0.0/theme.js"></script>
+        <script src="/webjars/momentjs/min/moment.min.js"></script>
+        <script type="module" src="/webjars/chihuahui/bundle.js"></script>
         <script th:src="@{/js/main.js}"></script>
 
         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
diff --git a/src/main/resources/templates/person/search.html b/src/main/resources/templates/person/search.html
index 6e1ef9505e7f7496768df2be1a2d271ec8beba70..6d23cc82700401bc8118ea3719dd8d95fef37c7a 100644
--- a/src/main/resources/templates/person/search.html
+++ b/src/main/resources/templates/person/search.html
@@ -67,7 +67,6 @@
                                 target="_blank"
                                 th:href="@{|/profile/${person.getKey().id}|}"
                                 th:text="${person.getKey().displayName}"></a>
-                            th:aria-label="|#{general.viewPerson.ariaLabel(${person.getKey().displayName})}|"
                         </div>
                     </td>
                     <td th:text="${person.getKey().number}"></td>
diff --git a/src/test/java/nl/tudelft/tam/controller/ApplicationControllerTest.java b/src/test/java/nl/tudelft/tam/controller/ApplicationControllerTest.java
index d6d6f5d78a32908383659f0143f5eb0a6abfdef2..ae614897c859d2caa6e45f6e9b463dc05159e6fd 100644
--- a/src/test/java/nl/tudelft/tam/controller/ApplicationControllerTest.java
+++ b/src/test/java/nl/tudelft/tam/controller/ApplicationControllerTest.java
@@ -164,16 +164,12 @@ class ApplicationControllerTest {
 		when(authService.getAuthPerson())
 				.thenReturn(Person.builder().defaultRole(DefaultRole.TEACHER).build());
 
-		List<Application> appListStart = List.of(mapper.map(app1a, Application.class),
-				mapper.map(app3a, Application.class));
-		appListStart.get(0).getJobOffer().setEditionId(EDITION_ID);
-		appListStart.get(1).getJobOffer().setEditionId(OLD_EDITION_ID);
-		List<Application> appListEnd = List.of(appListStart.get(0));
-
-		when(applicationService.getCoordinatingApplicationsForProgram(anyLong(), anyLong()))
-				.thenReturn(appListStart);
+		List<Application> appList = List.of(mapper.map(app1a, Application.class));
+		appList.get(0).getJobOffer().setEditionId(EDITION_ID);
+
+		when(applicationService.getFilteredCoordinatingApplications(anyLong(), any()))
+				.thenReturn(appList);
 		when(editionCacheManager.getOrThrow(eq(EDITION_ID))).thenReturn(edition);
-		when(editionCacheManager.getOrThrow(eq(OLD_EDITION_ID))).thenReturn(oldEdition);
 
 		PersonSummaryDTO person = new PersonSummaryDTO();
 		person.setDisplayName("Test");
@@ -185,7 +181,7 @@ class ApplicationControllerTest {
 		when(personCacheManager.getOrThrow(anyLong())).thenReturn(person);
 
 		Page<Application> expectedPage = new PageImpl<>(
-				appListEnd,
+				appList,
 				PageRequest.of(0, 25),
 				0);
 
@@ -194,12 +190,13 @@ class ApplicationControllerTest {
 				.andExpect(view().name("application/all"))
 				.andExpect(model().attribute("applications", expectedPage));
 
-		verify(applicationService).getCoordinatingApplicationsForProgram(anyLong(), anyLong());
+		verify(applicationService).getFilteredCoordinatingApplications(anyLong(), any());
 	}
 
 	@Test
 	@WithUserDetails("username")
 	void submitApplicationTest() throws Exception {
+		when(authService.canSubmitApplication(anyLong())).thenReturn(true);
 		when(applicationService.submit(anyLong(), any(ApplicationCreateDTO.class))).thenReturn(null);
 
 		String page = "job-offers";
@@ -216,6 +213,7 @@ class ApplicationControllerTest {
 	@Test
 	@WithUserDetails("username")
 	void submitApplicationTestEmptyPage() throws Exception {
+		when(authService.canSubmitApplication(anyLong())).thenReturn(true);
 		when(applicationService.submit(anyLong(), any(ApplicationCreateDTO.class))).thenReturn(null);
 
 		mvc.perform(post("/application/submit")
@@ -278,28 +276,28 @@ class ApplicationControllerTest {
 	@Test
 	@WithUserDetails("admin")
 	void promoteTest() throws Exception {
-		when(authService.isManagerOfAny()).thenReturn(true);
+		when(authService.isManagerOfJob(anyLong())).thenReturn(true);
 		doNothing().when(applicationService).promote(anyLong(), anyLong());
 
 		mvc.perform(post("/application/promote/{personId}/{offerId}", PERSON_ID_1, JOFFER_ID_1).with(csrf()))
 				.andExpect(status().is3xxRedirection())
 				.andExpect(redirectedUrl("/job-offer/" + JOFFER_ID_1));
 
-		verify(authService).isManagerOfAny();
+		verify(authService).isManagerOfJob(anyLong());
 		verify(applicationService).promote(PERSON_ID_1, JOFFER_ID_1);
 	}
 
 	@Test
 	@WithUserDetails("admin")
 	void demoteTest() throws Exception {
-		when(authService.isManagerOfAny()).thenReturn(true);
+		when(authService.isManagerOfJob(anyLong())).thenReturn(true);
 		doNothing().when(applicationService).promote(anyLong(), anyLong());
 
 		mvc.perform(post("/application/demote/{personId}/{offerId}", PERSON_ID_1, JOFFER_ID_1).with(csrf()))
 				.andExpect(status().is3xxRedirection())
 				.andExpect(redirectedUrl("/job-offer/" + JOFFER_ID_1));
 
-		verify(authService).isManagerOfAny();
+		verify(authService).isManagerOfJob(anyLong());
 		verify(applicationService).demote(PERSON_ID_1, JOFFER_ID_1);
 	}
 
@@ -321,14 +319,14 @@ class ApplicationControllerTest {
 	@Test
 	@WithUserDetails("admin")
 	void offerApplicationTest() throws Exception {
-		when(authService.isManagerOfAny()).thenReturn(true);
+		when(authService.isManagerOfJob(anyLong())).thenReturn(true);
 		doNothing().when(applicationService).offer(anyLong(), anyLong(), anyLong());
 
 		mvc.perform(post("/application/offer/{personId}/{offerId}", PERSON_ID_1, JOFFER_ID_1).with(csrf()))
 				.andExpect(status().is3xxRedirection())
 				.andExpect(redirectedUrl("/job-offer/" + JOFFER_ID_1));
 
-		verify(authService).isManagerOfAny();
+		verify(authService).isManagerOfJob(anyLong());
 		verify(applicationService).offer(PERSON_ID_1, JOFFER_ID_1, ADMIN_ID);
 	}
 
diff --git a/src/test/java/nl/tudelft/tam/controller/ControllerMockMvcTest.java b/src/test/java/nl/tudelft/tam/controller/ControllerMockMvcTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..7fad6d5ad9bc869714c4beae33861a01906c3ab2
--- /dev/null
+++ b/src/test/java/nl/tudelft/tam/controller/ControllerMockMvcTest.java
@@ -0,0 +1,53 @@
+/*
+ * TAM
+ * Copyright (C) 2021 - 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.tam.controller;
+
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.util.List;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+
+import application.test.TestTAMApplication;
+
+@AutoConfigureMockMvc
+@SpringBootTest(classes = TestTAMApplication.class)
+public abstract class ControllerMockMvcTest extends ControllerTest {
+
+	@Autowired
+	protected MockMvc mvc;
+
+	@ParameterizedTest
+	@MethodSource("protectedEndpoints")
+	void testWithoutUserDetailsIsForbidden(MockHttpServletRequestBuilder request) throws Exception {
+		mvc.perform(request.with(csrf()))
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("http://localhost/login"));
+	}
+
+	protected abstract List<MockHttpServletRequestBuilder> protectedEndpoints();
+
+}
diff --git a/src/test/java/nl/tudelft/tam/controller/ControllerTest.java b/src/test/java/nl/tudelft/tam/controller/ControllerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4f0cc2c4d1cd2852d73c1eb790559b1cb942d9c0
--- /dev/null
+++ b/src/test/java/nl/tudelft/tam/controller/ControllerTest.java
@@ -0,0 +1,50 @@
+/*
+ * TAM
+ * Copyright (C) 2021 - 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.tam.controller;
+
+import java.util.Random;
+
+import org.junit.jupiter.api.TestInstance;
+import org.modelmapper.ModelMapper;
+import org.modelmapper.convention.MatchingStrategies;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public abstract class ControllerTest {
+
+	private final Random random;
+	private final ModelMapper mapper;
+
+	public ControllerTest() {
+		random = new Random(seed());
+		mapper = new ModelMapper();
+		mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
+	}
+
+	public Long randomId() {
+		return random.nextLong(0L, 10000000L);
+	}
+
+	public <T> T map(Object source, Class<T> dest) {
+		return mapper.map(source, dest);
+	}
+
+	public Long seed() {
+		return 42L;
+	}
+
+}
diff --git a/src/test/java/nl/tudelft/tam/controller/EditionControllerTest.java b/src/test/java/nl/tudelft/tam/controller/EditionControllerTest.java
index 31b2e732205fff7aa2c4004125a879063a57fd12..28a35a70845aa33f564989576d7c63f6eb1c88ea 100644
--- a/src/test/java/nl/tudelft/tam/controller/EditionControllerTest.java
+++ b/src/test/java/nl/tudelft/tam/controller/EditionControllerTest.java
@@ -18,90 +18,62 @@
 package nl.tudelft.tam.controller;
 
 import static org.mockito.Mockito.*;
-import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
 
 import java.time.LocalDate;
 import java.time.LocalDateTime;
-import java.time.LocalTime;
 
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-import org.springframework.http.MediaType;
-import org.springframework.security.test.context.support.WithUserDetails;
-import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.transaction.annotation.Transactional;
 
-import application.test.TestTAMApplication;
 import nl.tudelft.labracore.api.dto.CohortIdDTO;
 import nl.tudelft.labracore.api.dto.CourseIdDTO;
 import nl.tudelft.labracore.api.dto.EditionCreateDTO;
+import nl.tudelft.labracore.lib.security.user.Person;
 import nl.tudelft.tam.dto.create.TamEditionCreateDTO;
-import nl.tudelft.tam.security.AuthorisationService;
 import nl.tudelft.tam.service.EditionService;
 import nl.tudelft.tam.service.RoleService;
 
-@AutoConfigureMockMvc
-@SpringBootTest(classes = TestTAMApplication.class)
 @Transactional
-public class EditionControllerTest {
+public class EditionControllerTest extends ControllerTest {
 
-	@Autowired
-	MockMvc mvc;
+	private final EditionController editionController;
 
-	@MockBean
-	EditionService editionService;
+	private final EditionService editionService;
+	private final RoleService roleService;
 
-	@MockBean
-	RoleService roleService;
+	public EditionControllerTest() {
+		this.editionService = mock(EditionService.class);
+		this.roleService = mock(RoleService.class);
 
-	@MockBean
-	AuthorisationService authService;
-
-	TamEditionCreateDTO tamEditionCreateDto;
-	EditionCreateDTO editionCreateDTO;
-
-	private long ADMIN_ID = 329476L;
-
-	@BeforeEach
-	void setup() {
-		tamEditionCreateDto = new TamEditionCreateDTO();
-		tamEditionCreateDto.setName("Test");
-		tamEditionCreateDto.setStartDate(LocalDate.now());
-		tamEditionCreateDto.setEndDate(LocalDate.now().plusYears(1));
-		tamEditionCreateDto.setCohort(new CohortIdDTO().id(1L));
-		tamEditionCreateDto.setCourse(new CourseIdDTO().id(1L));
-
-		editionCreateDTO = new EditionCreateDTO()
-				.name(tamEditionCreateDto.getName())
-				.course(tamEditionCreateDto.getCourse())
-				.cohort(tamEditionCreateDto.getCohort())
-				.enrollability(EditionCreateDTO.EnrollabilityEnum.OPEN)
-				.startDate(LocalDateTime.of(tamEditionCreateDto.getStartDate(), LocalTime.of(0, 0)))
-				.endDate(LocalDateTime.of(tamEditionCreateDto.getEndDate(), LocalTime.of(23, 59)));
+		this.editionController = new EditionController(editionService, roleService);
 	}
 
 	@Test
-	@WithUserDetails("admin")
-	void createEditionTest() throws Exception {
-		doReturn(true).when(authService).isManagerOfCourse(anyLong());
-
-		doReturn(1L).when(editionService).addEdition(any());
-		doNothing().when(editionService).addCohortToEdition(anyLong(), anyLong());
-		doNothing().when(roleService).changeToTeacher(anyLong(), anyLong());
-
-		mvc.perform(post("/edition/create").with(csrf())
-				.contentType(MediaType.APPLICATION_FORM_URLENCODED)
-				.flashAttr("tamEditionCreateDTO", tamEditionCreateDto))
-				.andExpect(status().is3xxRedirection())
-				.andExpect(redirectedUrl("/job-offer/manage"));
-
-		verify(editionService).addEdition(eq(editionCreateDTO));
-		verify(roleService).changeToTeacher(eq(ADMIN_ID), eq(1L));
+	void createEditionTest() {
+		Long personId = randomId();
+		Long editionId = randomId();
+		Long courseId = randomId();
+		Long cohortId = randomId();
+
+		when(editionService.addEdition(any())).thenReturn(editionId);
+
+		editionController.createEdition(Person.builder().id(personId).build(),
+				TamEditionCreateDTO.builder()
+						.name("Test Edition")
+						.course(new CourseIdDTO().id(courseId))
+						.cohort(new CohortIdDTO().id(cohortId))
+						.startDate(LocalDate.of(2023, 1, 1))
+						.endDate(LocalDate.of(2024, 1, 1))
+						.build());
+
+		verify(editionService).addEdition(new EditionCreateDTO()
+				.name("Test Edition")
+				.course(new CourseIdDTO().id(courseId))
+				.cohort(new CohortIdDTO().id(cohortId))
+				.enrollability(EditionCreateDTO.EnrollabilityEnum.OPEN)
+				.startDate(LocalDateTime.of(2023, 1, 1, 0, 0))
+				.endDate(LocalDateTime.of(2024, 1, 1, 23, 59)));
+		verify(roleService).changeToTeacher(personId, editionId);
 	}
 }
diff --git a/src/test/java/nl/tudelft/tam/controller/JobOfferControllerTest.java b/src/test/java/nl/tudelft/tam/controller/JobOfferControllerTest.java
index 6e1647c49e61af34208aff5f774142f553dfbae3..1bb6cd40d88f716ad4f588556f0bbb86f3cef137 100644
--- a/src/test/java/nl/tudelft/tam/controller/JobOfferControllerTest.java
+++ b/src/test/java/nl/tudelft/tam/controller/JobOfferControllerTest.java
@@ -481,6 +481,7 @@ class JobOfferControllerTest {
 		when(offerService.getById(anyLong())).thenReturn(offer1);
 		when(applicationService.getFilteredApplicationsForJobOffer(anyLong(), any()))
 				.thenReturn(List.of(ap1, ap2));
+		when(courseService.getCourseById(anyLong())).thenReturn(new CourseDetailsDTO().program(new ProgramSummaryDTO().id(1L)));
 
 
 		int[] stats = { 2, 1, 0 };
@@ -492,8 +493,7 @@ class JobOfferControllerTest {
 				.andExpect(model().attribute("offer", offer1))
 				.andExpect(model().attribute("deadlineClosed", false))
 				.andExpect(model().attribute("applications", List.of(ap1, ap2)))
-				.andExpect(model().attribute("stats", stats))
-				.andExpect(model().attributeExists("patchJobOffer"));
+				.andExpect(model().attribute("stats", stats));
 
 		verify(offerService).getById(JOFFER_ID_1);
 		verify(applicationService).getFilteredApplicationsForJobOffer(eq(JOFFER_ID_1), any());
@@ -503,7 +503,7 @@ class JobOfferControllerTest {
 	@Test
 	@WithUserDetails("admin")
 	void createNewJobOfferTest() throws Exception {
-		when(authService.isManagerOfAny()).thenReturn(true);
+		when(authService.isManagerOfEdition(anyLong())).thenReturn(true);
 
 		JobOfferCreateDTO createDto = JobOfferCreateDTO.builder().name("test-name").editionId(EDITION_ID)
 				.deadline(LocalDate.now().plusDays(42)).maxHours(100).build();
@@ -517,7 +517,7 @@ class JobOfferControllerTest {
 				.andExpect(status().is3xxRedirection())
 				.andExpect(redirectedUrl("/job-offer/" + newId));
 
-		verify(authService).isManagerOfAny();
+		verify(authService).isManagerOfEdition(anyLong());
 		verify(offerService).newJobOffer(createDto);
 	}
 
diff --git a/src/test/java/nl/tudelft/tam/repository/JobOfferRepositoryTest.java b/src/test/java/nl/tudelft/tam/repository/JobOfferRepositoryTest.java
index 92170399c0df8f3fb5f7360d94cc57bf4fd8d538..c8eda1e504683d55644c9aac8db2380d0cb61368 100644
--- a/src/test/java/nl/tudelft/tam/repository/JobOfferRepositoryTest.java
+++ b/src/test/java/nl/tudelft/tam/repository/JobOfferRepositoryTest.java
@@ -28,12 +28,14 @@ import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.data.rest.webmvc.ResourceNotFoundException;
+import org.springframework.transaction.annotation.Transactional;
 
 import application.test.TestTAMApplication;
 import nl.tudelft.tam.enums.AcademicLevel;
 import nl.tudelft.tam.enums.AcademicPeriod;
 import nl.tudelft.tam.model.JobOffer;
 
+@Transactional
 @SpringBootTest(classes = TestTAMApplication.class)
 class JobOfferRepositoryTest {
 
diff --git a/src/test/java/nl/tudelft/tam/service/ApplicationServiceMockTest.java b/src/test/java/nl/tudelft/tam/service/ApplicationServiceMockTest.java
index feb76b7808d5702dfc6066bc236910b024ed4c27..226705fcb352788f059298a7af263f58d9d4236b 100644
--- a/src/test/java/nl/tudelft/tam/service/ApplicationServiceMockTest.java
+++ b/src/test/java/nl/tudelft/tam/service/ApplicationServiceMockTest.java
@@ -423,13 +423,13 @@ class ApplicationServiceMockTest {
 		when(repository.findByIdOrThrow(app1b.getId())).thenReturn(app1b);
 		assertThat(app1b.getStatus()).isEqualTo(Status.OFFERED);
 
-		doNothing().when(roleService).changeToTa(anyLong(), anyLong());
+		doNothing().when(roleService).changeToTa(anyLong(), anyLong(), anyBoolean());
 
 		service.accept(PERSON_ID_2, JOFFER_ID_1);
 
 		assertThat(app1b.getStatus()).isEqualTo(Status.ACCEPTED);
 
-		verify(roleService).changeToTa(PERSON_ID_2, JOFFER_ID_1);
+		verify(roleService).changeToTa(PERSON_ID_2, JOFFER_ID_1, false);
 	}
 
 	@Test
@@ -461,13 +461,13 @@ class ApplicationServiceMockTest {
 		when(repository.findByIdOrThrow(app1b.getId())).thenReturn(app1b);
 		assertThat(app1b.getStatus()).isEqualTo(Status.ACCEPTED);
 
-		doNothing().when(roleService).changeToTa(anyLong(), anyLong());
+		doNothing().when(roleService).changeToTa(anyLong(), anyLong(), anyBoolean());
 
 		service.demote(PERSON_ID_2, JOFFER_ID_1);
 
 		assertThat(app1b.getStatus()).isEqualTo(Status.ACCEPTED);
 
-		verify(roleService).changeToTa(PERSON_ID_2, JOFFER_ID_1);
+		verify(roleService).changeToTa(PERSON_ID_2, JOFFER_ID_1, true);
 	}
 
 	@Test
@@ -475,7 +475,7 @@ class ApplicationServiceMockTest {
 		when(repository.findByIdOrThrow(app1b.getId())).thenReturn(app1b);
 		assertThrows(IllegalStateException.class, () -> service.demote(PERSON_ID_2, JOFFER_ID_1));
 
-		verify(roleService, never()).changeToTa(anyLong(), anyLong());
+		verify(roleService, never()).changeToTa(anyLong(), anyLong(), anyBoolean());
 	}
 
 	@Test
@@ -724,6 +724,7 @@ class ApplicationServiceMockTest {
 		assertThat(captor.getValue())
 				.contains(
 						new String[] { "Name",
+								"NetID",
 								"Email",
 								"Contract Name",
 								"Baan Code",
@@ -738,6 +739,7 @@ class ApplicationServiceMockTest {
 								"Remarks" },
 
 						new String[] { "Person 1",
+								"Person1",
 								"Person1@email.com",
 								null,
 								null,
@@ -752,6 +754,7 @@ class ApplicationServiceMockTest {
 								"" },
 
 						new String[] { "Person 2",
+								"Person2",
 								"Person2@email.com",
 								null,
 								null,
diff --git a/src/test/java/nl/tudelft/tam/service/JobOfferServiceTest.java b/src/test/java/nl/tudelft/tam/service/JobOfferServiceTest.java
index c6531d6f00f7e4d5e22b6b02d5680102db9ab076..4322260b53ce8118d3475bcdc1a312cb80ce1596 100644
--- a/src/test/java/nl/tudelft/tam/service/JobOfferServiceTest.java
+++ b/src/test/java/nl/tudelft/tam/service/JobOfferServiceTest.java
@@ -164,30 +164,42 @@ class JobOfferServiceTest {
 	@Test
 	void newJobOffer() {
 		when(repository.save(any())).thenReturn(jobOffer1);
+		when(editionService.getEditionById(anyLong())).thenReturn(new EditionDetailsDTO().course(new CourseSummaryDTO().id(1L)));
+		when(courseService.getCourseById(anyLong())).thenReturn(new CourseDetailsDTO().program(new ProgramSummaryDTO().id(1L)));
+
 		JobOfferCreateDTO createDTO = JobOfferCreateDTO.builder().editionId(jobOffer1.getEditionId())
 				.maxHours(100).build();
 
 		assertThat(service.newJobOffer(createDTO)).isEqualTo(jobOffer1.getId());
+
 		verify(repository).save(createDTO.apply());
 	}
 
 	@Test
 	void newJobOfferSetNotDraft() {
 		when(repository.save(any())).thenReturn(jobOffer1);
+		when(editionService.getEditionById(anyLong())).thenReturn(new EditionDetailsDTO().course(new CourseSummaryDTO().id(1L)));
+		when(courseService.getCourseById(anyLong())).thenReturn(new CourseDetailsDTO().program(new ProgramSummaryDTO().id(1L)));
+
 		JobOfferCreateDTO createDTO = JobOfferCreateDTO.builder().editionId(jobOffer1.getEditionId())
 				.hidden(false).maxHours(100).build();
 
 		assertThat(service.newJobOffer(createDTO)).isEqualTo(jobOffer1.getId());
+
 		verify(repository).save(createDTO.apply());
 	}
 
 	@Test
 	void newDraftJobOffer() {
 		when(repository.save(any())).thenReturn(jobOffer1);
+		when(editionService.getEditionById(anyLong())).thenReturn(new EditionDetailsDTO().course(new CourseSummaryDTO().id(1L)));
+		when(courseService.getCourseById(anyLong())).thenReturn(new CourseDetailsDTO().program(new ProgramSummaryDTO().id(1L)));
+
 		JobOfferCreateDTO createDTO = JobOfferCreateDTO.builder().editionId(jobOffer1.getEditionId())
 				.hidden(true).maxHours(100).build();
 
 		assertThat(service.newJobOffer(createDTO)).isEqualTo(jobOffer1.getId());
+
 		verify(repository).save(createDTO.apply());
 	}
 
@@ -365,6 +377,8 @@ class JobOfferServiceTest {
 				List.of());
 		when(cohortService.getCohortsById(any())).thenReturn(
 				List.of());
+		when(editionService.getEditionById(anyLong())).thenReturn(new EditionDetailsDTO().course(new CourseSummaryDTO().id(1L)));
+		when(courseService.getCourseById(anyLong())).thenReturn(new CourseDetailsDTO().program(new ProgramSummaryDTO().id(1L)));
 
 		String content = """
 				Edition,Course Code,Job Name,Contract Name,Start Date,End Date,Baancode,Max Hours,Hiring Message,Rejection Message,Deadline
diff --git a/src/test/java/nl/tudelft/tam/service/RoleServiceTest.java b/src/test/java/nl/tudelft/tam/service/RoleServiceTest.java
index 795a462058e2a215adde10213ce6db72aab21688..c14f1e1f6882af275751d51b7b3ecd3ea0b6deb2 100644
--- a/src/test/java/nl/tudelft/tam/service/RoleServiceTest.java
+++ b/src/test/java/nl/tudelft/tam/service/RoleServiceTest.java
@@ -32,6 +32,7 @@ import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.transaction.annotation.Transactional;
 
 import application.test.TestTAMApplication;
+import nl.tudelft.labracore.api.EditionControllerApi;
 import nl.tudelft.labracore.api.RoleControllerApi;
 import nl.tudelft.labracore.api.dto.*;
 import reactor.core.publisher.Flux;
@@ -56,6 +57,9 @@ public class RoleServiceTest {
 	@Autowired
 	RoleControllerApi roleControllerApi;
 
+	@Autowired
+	EditionControllerApi editionControllerApi;
+
 	final long JOFFER_ID = 1001;
 	final long PERSON_ID = 5001;
 	final long EDITION_ID = 7000;
@@ -122,7 +126,55 @@ public class RoleServiceTest {
 		when(roleControllerApi.patchRole(any(), anyLong(), anyLong()))
 				.thenReturn(Mono.just(new Id().personId(PERSON_ID).editionId(EDITION_ID)));
 
-		service.changeToTa(PERSON_ID, JOFFER_ID);
+		when(editionControllerApi.getEditionParticipants(anyLong())).thenReturn(
+				Flux.just(new RolePersonDetailsDTO().person(new PersonSummaryDTO().id(PERSON_ID)))
+		);
+
+		service.changeToTa(PERSON_ID, JOFFER_ID, false);
+
+		verify(roleControllerApi).patchRole(new RolePatchDTO().type(RolePatchDTO.TypeEnum.TA), PERSON_ID,
+				EDITION_ID);
+	}
+
+	@Test
+	void changeToTaPatchRole_HeadTaAlready() {
+		when(jobOfferService.getEditionFromJobOffer(JOFFER_ID)).thenReturn(EDITION_ID);
+
+		when(personService.getRolesForPerson(anyLong())).thenReturn(List.of(roleCorrectEdition));
+
+		when(roleControllerApi.patchRole(any(), anyLong(), anyLong()))
+				.thenReturn(Mono.just(new Id().personId(PERSON_ID).editionId(EDITION_ID)));
+
+		when(editionControllerApi.getEditionParticipants(anyLong())).thenReturn(
+				Flux.just(new RolePersonDetailsDTO()
+						.person(new PersonSummaryDTO().id(PERSON_ID))
+						.type(RolePersonDetailsDTO.TypeEnum.HEAD_TA)
+				)
+		);
+
+		service.changeToTa(PERSON_ID, JOFFER_ID, false);
+
+		verify(roleControllerApi, times(0)).patchRole(new RolePatchDTO().type(RolePatchDTO.TypeEnum.TA), PERSON_ID,
+				EDITION_ID);
+	}
+
+	@Test
+	void changeToTaPatchRole_HeadTaAlready_Demote() {
+		when(jobOfferService.getEditionFromJobOffer(JOFFER_ID)).thenReturn(EDITION_ID);
+
+		when(personService.getRolesForPerson(anyLong())).thenReturn(List.of(roleCorrectEdition));
+
+		when(roleControllerApi.patchRole(any(), anyLong(), anyLong()))
+				.thenReturn(Mono.just(new Id().personId(PERSON_ID).editionId(EDITION_ID)));
+
+		when(editionControllerApi.getEditionParticipants(anyLong())).thenReturn(
+				Flux.just(new RolePersonDetailsDTO()
+						.person(new PersonSummaryDTO().id(PERSON_ID))
+						.type(RolePersonDetailsDTO.TypeEnum.HEAD_TA)
+				)
+		);
+
+		service.changeToTa(PERSON_ID, JOFFER_ID, true);
 
 		verify(roleControllerApi).patchRole(new RolePatchDTO().type(RolePatchDTO.TypeEnum.TA), PERSON_ID,
 				EDITION_ID);
@@ -137,7 +189,7 @@ public class RoleServiceTest {
 		when(roleControllerApi.addRole(any()))
 				.thenReturn(Mono.just(new Id().personId(PERSON_ID).editionId(EDITION_ID)));
 
-		service.changeToTa(PERSON_ID, JOFFER_ID);
+		service.changeToTa(PERSON_ID, JOFFER_ID, false);
 
 		verify(roleControllerApi).addRole(new RoleCreateDTO()
 				.edition(new EditionIdDTO().id(EDITION_ID))