Skip to content

feat: behaviour control #69

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ data class StopNotificationContext(
val stopTitle: String,
)

data class PupilNotificationContext(
val classroomTitle: String,
val pupilDisplayName: String,
)

/**
* Resolves domain context needed to build teacher notification render arguments.
*/
Expand Down Expand Up @@ -75,6 +80,23 @@ class NotificationEventContextResolver(
)
}

fun resolvePupilContext(
classroomId: Long,
pupilUserId: Long,
): PupilNotificationContext {
val classroom = classroomRepository.findById(classroomId)
.orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Classroom not found") }

val pupilProfile = pupilProfileRepository.findById(pupilUserId).orElse(null)
val pupil = userRepository.findById(pupilUserId)
.orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Pupil not found") }

return PupilNotificationContext(
classroomTitle = classroom.title,
pupilDisplayName = pupilProfile?.displayName ?: pupil.username ?: "Elev",
)
}

/**
* Serializes a small ordered key-value payload to JSON for frontend rendering arguments.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import edu.ntnu.idi.idatt.backend.shared.events.MapStopCompletedEvent
import edu.ntnu.idi.idatt.backend.shared.events.PupilGameCompletedEvent
import edu.ntnu.idi.idatt.backend.shared.events.PupilRecoveredFromStuckEvent
import edu.ntnu.idi.idatt.backend.shared.events.PupilStuckThresholdReachedEvent
import edu.ntnu.idi.idatt.backend.shared.events.InappropriateBehaviourEvent
import org.springframework.stereotype.Component
import org.springframework.transaction.event.TransactionPhase
import org.springframework.transaction.event.TransactionalEventListener
Expand Down Expand Up @@ -146,6 +147,48 @@ class TeacherNotificationEventListener(
)
}

/**
* Materializes an inappropriate behaviour notification after a pupil attempts to write banned words.
*
* @param event committed inappropriate behaviour business event
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun onInappropriateBehaviour(event: InappropriateBehaviourEvent) {
val context =
notificationEventContextResolver.resolvePupilContext(
classroomId = event.classroomId,
pupilUserId = event.pupilUserId,
)
val threadKey = inappropriateBehaviourThreadKey(
event.classroomId,
event.pupilUserId,
)

notificationDeliveryService.deliverToClassroomTeachers(
NotificationDraft(
classroomId = event.classroomId,
pupilUserId = event.pupilUserId,
type = NotificationType.INAPPROPRIATE_BEHAVIOUR,
threadKey = threadKey,
renderCode = NotificationRenderCode.INAPPROPRIATE_BEHAVIOR,
renderArgsJson =
notificationEventContextResolver.json(
"classroomTitle" to context.classroomTitle,
"pupilDisplayName" to context.pupilDisplayName,
"input" to event.input,
"context" to event.context,
),
payloadJson =
notificationEventContextResolver.json(
"classroomId" to event.classroomId,
"pupilUserId" to event.pupilUserId,
"input" to event.input,
"context" to event.context,
),
),
)
}

companion object {
/**
* Stable thread key for stop-completion notifications for one pupil/stop/classroom tuple.
Expand Down Expand Up @@ -186,5 +229,19 @@ class TeacherNotificationEventListener(
pupilUserId: Long,
stopId: Long,
): String = "stuck:$classroomId:$pupilUserId:$stopId"


/**
* Stable thread key for inappropriate behavior notifications for one pupil/classroom tuple.
*
* @param classroomId classroom that scopes the notification thread
* @param pupilUserId pupil that has behaved inappropriately
* @return deterministic notification thread key
*/
fun inappropriateBehaviourThreadKey(
classroomId: Long,
pupilUserId: Long,
): String = "inappropriate:$classroomId:$pupilUserId"
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ enum class NotificationRenderCode(
GAME_COMPLETED("notifications.game-completed"),
PUPIL_STUCK("notifications.pupil-stuck"),
WEEKLY_MYSTERY_SUBMITTED("notifications.weekly-mystery-submitted"),
INAPPROPRIATE_BEHAVIOR("notifications.inappropriate-behavior"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ enum class NotificationType {
GAME_COMPLETED,
PUPIL_STUCK,
WEEKLY_MYSTERY_SUBMITTED,
INAPPROPRIATE_BEHAVIOUR,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package edu.ntnu.idi.idatt.backend.shared.events

import java.time.Instant

data class InappropriateBehaviourEvent(
val pupilUserId: Long,
val classroomId: Long,
val input: String,
val context: String,
override val occurredAt: Instant = Instant.now(),

) : DomainEvent

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ CREATE TABLE teacher_notifications
type ENUM('MAP_STOP_COMPLETED',
'GAME_COMPLETED',
'PUPIL_STUCK',
'WEEKLY_MYSTERY_SUBMITTED'
'WEEKLY_MYSTERY_SUBMITTED',
'INAPPROPRIATE_BEHAVIOUR',
) NOT NULL,
thread_key VARCHAR(255),
active_thread_key VARCHAR(255),
Expand Down
Loading