Skip to content

Commit

Permalink
feat: teacher overview classroom and status (#79)
Browse files Browse the repository at this point in the history
* Add teacher email handling in classroom creation

* Update tests

* Add status and ownerTeacherId to classroom models and update handling

* Format test
  • Loading branch information
johanaam authored Apr 30, 2026
1 parent fd66d04 commit 6f1f7f9
Show file tree
Hide file tree
Showing 11 changed files with 102 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import edu.ntnu.idi.idatt.backend.classroom.application.JoinClassroomResult
import edu.ntnu.idi.idatt.backend.classroom.application.OwnedClassroomSummary
import edu.ntnu.idi.idatt.backend.classroom.application.UpdateClassroomCommand
import edu.ntnu.idi.idatt.backend.classroom.domain.Classroom
import edu.ntnu.idi.idatt.backend.iam.domain.User

/**
* Converts the join classroom request to the application command.
Expand All @@ -27,6 +28,7 @@ fun CreateClassroomRequest.toCommand(subject: String): CreateClassroomCommand =
CreateClassroomCommand(
title = title,
description = description,
teacherEmails = teacherEmails,
authenticatedSubject = subject,
)

Expand All @@ -44,6 +46,7 @@ fun UpdateClassroomRequest.toCommand(
title = title,
description = description,
teacherEmails = teacherEmails,
status = status,
authenticatedSubject = authenticatedSubject,
)

Expand Down Expand Up @@ -71,6 +74,9 @@ fun Classroom.toResponse(): ClassroomResponse =
title = title,
description = description,
joinCode = joinCode ?: "",
status = status.name,
ownerTeacherId = ownerTeacherId,
teachers = teacherResponses(),
teacherUserIds = teacherMemberships.mapNotNull { it.teacher?.id },
)

Expand All @@ -88,15 +94,9 @@ fun Classroom.toDetailResponse(
title = title,
description = description,
joinCode = joinCode ?: "",
teachers =
teacherMemberships.mapNotNull { membership ->
val teacher = membership.teacher ?: return@mapNotNull null

ClassroomTeacherResponse(
userId = teacher.id ?: return@mapNotNull null,
email = teacher.email,
)
},
status = status.name,
ownerTeacherId = ownerTeacherId,
teachers = teacherResponses(),
pupils =
pupilMemberships.mapNotNull { membership ->
val pupil = membership.pupil ?: return@mapNotNull null
Expand All @@ -112,6 +112,22 @@ fun Classroom.toDetailResponse(
},
)

private fun Classroom.teacherResponses(): List<ClassroomTeacherResponse> =
teacherMemberships.mapNotNull { membership ->
val teacher = membership.teacher ?: return@mapNotNull null
teacher.toClassroomTeacherResponse(ownerTeacherId)
}

private fun User.toClassroomTeacherResponse(ownerTeacherId: Long): ClassroomTeacherResponse? {
val userId = id ?: return null

return ClassroomTeacherResponse(
userId = userId,
email = email,
isOwner = userId == ownerTeacherId,
)
}

fun OwnedClassroomSummary.toOwnedClassroomResponse(): OwnedClassroomResponse =
OwnedClassroomResponse(
id = id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ package edu.ntnu.idi.idatt.backend.classroom.api
* @param title the title of the classroom.
* @param description the description of the classroom.
* @param joinCode the code for joining the classroom.
* @param status whether the classroom is active or inactive.
* @param ownerTeacherId the id of the teacher who owns the classroom.
* @param teachers a list of the teachers that are members of the classroom.
* @param pupils a list of the pupils that are members of the classroom.
*/
Expand All @@ -15,6 +17,8 @@ data class ClassroomDetailResponse(
val title: String,
val description: String?,
val joinCode: String,
val status: String,
val ownerTeacherId: Long,
val teachers: List<ClassroomTeacherResponse>,
val pupils: List<ClassroomPupilResponse>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ package edu.ntnu.idi.idatt.backend.classroom.api
* @param title the title of a classroom.
* @param description the optional description of a classroom.
* @param joinCode the code to join the classroom.
* @param teacherUserIds a list of ids of teahcers belonging to the classroom.
* @param status whether the classroom is active or inactive.
* @param ownerTeacherId the id of the teacher who owns the classroom.
* @param teachers a list of teachers belonging to the classroom.
* @param teacherUserIds a list of ids of teachers belonging to the classroom.
*/
data class ClassroomResponse(
val classroomId: Long,
val title: String,
val description: String?,
val joinCode: String,
val status: String,
val ownerTeacherId: Long,
val teachers: List<ClassroomTeacherResponse>,
val teacherUserIds: List<Long>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ package edu.ntnu.idi.idatt.backend.classroom.api
*
* @param userId the id of a teacher in the classroom.
* @param email the email address of a teacher in the classroom.
* @param isOwner whether this teacher owns the classroom.
*/
data class ClassroomTeacherResponse(
val userId: Long,
val email: String?,
val isOwner: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import jakarta.validation.constraints.Size
*
* @property title the title of the classroom to be created. Must not be blank.
* @property description the optional description of the classroom.
* @property teacherEmails a list of emails of teachers belonging to the classroom.
*/
data class CreateClassroomRequest(
@field:NotBlank
@field:Size(max = 100)
val title: String,
val description: String? = null,
val teacherEmails: List<String> = emptyList(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import jakarta.validation.constraints.NotBlank
* @property title the title of the classroom. Must not be blank.
* @property description the description of the classroom.
* @property teacherEmails a list of emails of teachers belonging to the classroom.
* @property status whether the classroom should be active or inactive.
*/
data class UpdateClassroomRequest(
@field:NotBlank
val title: String,
val description: String? = null,
val teacherEmails: List<String> = emptyList(),
val status: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ class ClassroomService(
fun createClassroom(command: CreateClassroomCommand): Classroom {
val teacher = currentUserResolver.requireTeacherOrAdmin(command.authenticatedSubject)
val joinCode = generateJoinCode()
val currentTeacherEmail = teacher.email?.trim()?.lowercase(Locale.ROOT)
val emails =
command.teacherEmails
.map { it.trim().lowercase(Locale.ROOT) }
.filter { it.isNotBlank() }
.filter { it != currentTeacherEmail }
.distinct()

val teachers = iamUserService.findTeachersByEmails(emails)

if (teachers.size != emails.size) {
val foundEmails = teachers.mapNotNull { it.email?.lowercase(Locale.ROOT) }
val missing = emails - foundEmails.toSet()
throw ResponseStatusException(HttpStatus.CONFLICT, "Teachers not found: $missing")
}

val classroom =
Classroom(
Expand All @@ -63,6 +78,7 @@ class ClassroomService(
)

classroom.addTeacher(teacher)
teachers.forEach(classroom::addTeacher)

return classroomRepository.save(classroom)
}
Expand Down Expand Up @@ -228,8 +244,24 @@ class ClassroomService(

classroom.title = command.title
classroom.description = command.description

val emails = command.teacherEmails.map { it.trim().lowercase(Locale.ROOT) }
classroom.status =
command.status?.let { status ->
runCatching { ClassroomStatus.valueOf(status.trim().uppercase(Locale.ROOT)) }
.getOrElse {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid classroom status: $status")
}
} ?: classroom.status

val owner =
iamUserService.findById(classroom.ownerTeacherId)
?: throw ResponseStatusException(HttpStatus.CONFLICT, "Classroom owner not found")
val ownerEmail = owner.email?.trim()?.lowercase(Locale.ROOT)
val emails =
command.teacherEmails
.map { it.trim().lowercase(Locale.ROOT) }
.filter { it.isNotBlank() }
.filter { it != ownerEmail }
.distinct()

val teachers = iamUserService.findTeachersByEmails(emails)

Expand All @@ -241,6 +273,7 @@ class ClassroomService(

classroom.teacherMemberships.clear()

classroom.addTeacher(owner)
teachers.forEach { teacher ->
classroom.addTeacher(teacher)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ package edu.ntnu.idi.idatt.backend.classroom.application
*
* @property title the title of the classroom.
* @property description the optional description of the classroom.
* @property teacherEmails the list of teachers to add to the classroom.
* @property authenticatedSubject JWT subject for the authenticated subject user attempting to create the classroom.
*/
data class CreateClassroomCommand(
val title: String,
val description: String?,
val teacherEmails: List<String>,
val authenticatedSubject: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ package edu.ntnu.idi.idatt.backend.classroom.application
* @property title the updated title of the classroom.
* @property description the updated description of the classroom.
* @property teacherEmails the updated list of emails of teachers members of the classroom.
* @property status the updated classroom status.
* @property authenticatedSubject JWT subject for the authenticated subject user attempting to update the classroom.
*/
data class UpdateClassroomCommand(
val classroomId: Long,
val title: String,
val description: String?,
val teacherEmails: List<String>,
val status: String?,
val authenticatedSubject: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import edu.ntnu.idi.idatt.backend.users.avatar.infrastructure.UserAvatarReposito
import edu.ntnu.idi.idatt.backend.users.profile.domain.PupilProfile
import edu.ntnu.idi.idatt.backend.users.profile.infrastructure.PupilProfileRepository
import edu.ntnu.idi.idatt.backend.users.profile.infrastructure.TeacherProfileRepository
import org.hamcrest.Matchers.hasItem
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
Expand Down Expand Up @@ -346,6 +347,9 @@ class ClassroomIntegrationTests {
status { isOk() }
jsonPath("$.title") { value("Test Classroom") }
jsonPath("$.description") { value("Test Description") }
jsonPath("$.status") { value("ACTIVE") }
jsonPath("$.teachers[*].email") { value(hasItem("teacher-create@test.com")) }
jsonPath("$.teacherUserIds[0]") { exists() }
}
}

Expand All @@ -355,13 +359,15 @@ class ClassroomIntegrationTests {

val ownerEmail = "owner-UPDATE1@example.com"
val token = teacherPortalAccessToken(ownerEmail, "secret123")
userRepository.save(activeTeacher("teacher-update@test.com"))

val request =
"""
{
"title": "Updated Classroom",
"description": "Updated Classroom Description",
"teacherEmails": []
"teacherEmails": ["teacher-update@test.com"],
"status": "INACTIVE"
}
""".trimIndent()

Expand All @@ -375,6 +381,10 @@ class ClassroomIntegrationTests {
status { isOk() }
jsonPath("$.title") { value("Updated Classroom") }
jsonPath("$.description") { value("Updated Classroom Description") }
jsonPath("$.status") { value("INACTIVE") }
jsonPath("$.teachers[*].email") { value(hasItem(ownerEmail.lowercase())) }
jsonPath("$.teachers[*].email") { value(hasItem("teacher-update@test.com")) }
jsonPath("$.teacherUserIds[0]") { exists() }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class ClassroomServiceTest {
CreateClassroomCommand(
title = "Math",
description = "description",
teacherEmails = emptyList(),
authenticatedSubject = "1",
),
)
Expand Down Expand Up @@ -342,14 +343,17 @@ class ClassroomServiceTest {

@Test
fun `should update classroom title, description and teachers`() {
val classroom = Classroom(id = 1, ownerTeacherId = 2, title = "old", description = "old", joinCode = "ABC123", status = ClassroomStatus.ACTIVE)
val classroom = Classroom(id = 1, ownerTeacherId = 10, title = "old", description = "old", joinCode = "ABC123", status = ClassroomStatus.ACTIVE)
val teacher = User(id = 10, email = "teacher@test.com", username = "teacher", passwordHash = "password", role = UserRole.TEACHER, status = UserStatus.ACTIVE)

`when`(currentUserResolver.requireTeacherOrAdmin("10")).thenReturn(teacher)
`when`(classroomUserResolver.requireClassroomOwner(teacher, 1L)).thenReturn(classroom)
`when`(classroomRepository.findById(1)).thenReturn(Optional.of(classroom))
`when`(userRepository.findAllByEmailIgnoreCaseIn(listOf("teacher@test.com"))).thenReturn(listOf(teacher))
`when`(userRepository.findById(10)).thenReturn(Optional.of(teacher))
`when`(userRepository.findAllByEmailIgnoreCaseIn(emptyList())).thenReturn(emptyList())
`when`(classroomRepository.save(any())).thenAnswer { it.arguments[0] }

val command = UpdateClassroomCommand(classroomId = 1, title = "new", description = "new", teacherEmails = listOf("teacher@test.com"), authenticatedSubject = "10")
val command = UpdateClassroomCommand(classroomId = 1, title = "new", description = "new", teacherEmails = listOf("teacher@test.com"), status = null, authenticatedSubject = "10")

val result = classroomService.updateClassroom(command)

Expand All @@ -366,12 +370,13 @@ class ClassroomServiceTest {
val teacher = User(id = 2, email = "test@test.com", username = "teacher", passwordHash = "password", role = UserRole.TEACHER, status = UserStatus.ACTIVE)

`when`(currentUserResolver.requireTeacherOrAdmin("teacher")).thenReturn(teacher)
`when`(classroomUserResolver.requireClassroomOwner(teacher, 1L)).thenAnswer { }
`when`(classroomUserResolver.requireClassroomOwner(teacher, 1L)).thenReturn(classroom)

`when`(classroomRepository.findById(1)).thenReturn(Optional.of(classroom))
`when`(userRepository.findById(2)).thenReturn(Optional.of(teacher))
`when`(userRepository.findAllByEmailIgnoreCaseIn(listOf("missing@test.com"))).thenReturn(emptyList())

val command = UpdateClassroomCommand(classroomId = 1, title = "new", description = "new", teacherEmails = listOf("missing@test.com"), authenticatedSubject = "2")
val command = UpdateClassroomCommand(classroomId = 1, title = "new", description = "new", teacherEmails = listOf("missing@test.com"), status = null, authenticatedSubject = "2")

val exception =
org.junit.jupiter.api.Assertions.assertThrows(ResponseStatusException::class.java) {
Expand Down

0 comments on commit 6f1f7f9

Please sign in to comment.