diff --git a/backend/src/main/kotlin/uk/co/neviyn/observationdatabase/Api.kt b/backend/src/main/kotlin/uk/co/neviyn/observationdatabase/Api.kt index f0599a1..920729b 100644 --- a/backend/src/main/kotlin/uk/co/neviyn/observationdatabase/Api.kt +++ b/backend/src/main/kotlin/uk/co/neviyn/observationdatabase/Api.kt @@ -25,6 +25,18 @@ data class NewObservation( val person: String ) +data class GroupObservationInit( + val site: Long, + val type: TrainingType, + val tutors: List, + val scenarioTitles: List +) + +data class GroupObservation( + val scenarios: List, + val person: String +) + data class ObservationsRequest( val site: Long?, val tutor: Long?, diff --git a/backend/src/main/kotlin/uk/co/neviyn/observationdatabase/Controller.kt b/backend/src/main/kotlin/uk/co/neviyn/observationdatabase/Controller.kt index 6c0e69c..fd73c99 100644 --- a/backend/src/main/kotlin/uk/co/neviyn/observationdatabase/Controller.kt +++ b/backend/src/main/kotlin/uk/co/neviyn/observationdatabase/Controller.kt @@ -1,18 +1,21 @@ package uk.co.neviyn.observationdatabase import org.joda.time.LocalDate +import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.cache.annotation.Cacheable +import org.springframework.core.env.Environment +import org.springframework.core.env.get import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController -import org.springframework.messaging.simp.SimpMessagingTemplate import org.springframework.web.bind.annotation.CrossOrigin -import java.net.InetAddress +import java.net.Inet4Address +import java.net.NetworkInterface import javax.validation.Valid @RestController @@ -20,10 +23,10 @@ import javax.validation.Valid @CrossOrigin class Controller { - val logger = LoggerFactory.getLogger(javaClass) + private val logger: Logger = LoggerFactory.getLogger(javaClass)!! @Autowired - lateinit var websocketMessenger: SimpMessagingTemplate + lateinit var environment: Environment @Autowired lateinit var siteRepository: SiteRepository @@ -85,6 +88,18 @@ class Controller { return nameValue } + fun saveObservation(observation: Observation): Observation { + logger.debug("Saving new Observation to database") + val committedObservation = observationRepository.save(observation) + logger.debug("Adding Observation data to Tutor records") + committedObservation.tutors.forEach { + it.observations.add(committedObservation) + tutorRepository.save(it) + } + logger.debug("Observation addition completed") + return committedObservation + } + /** * Add a new observation to the database using data provided in [newObservation]. */ @@ -118,14 +133,7 @@ class Controller { person = personRepository.findFirstByNameLike(newObservation.person.toUpperCase()) ?: personRepository.save(Person(name = newObservation.person.toUpperCase())) ) logger.debug("Saving new Observation to database") - observation = observationRepository.save(observation) - logger.debug("Adding Observation data to Tutor records") - tutors.forEach { - it.observations.add(observation) - tutorRepository.save(it) - } - sendObservationToSocket(observation) - logger.debug("Observation addition completed") + observation = saveObservation(observation) return observation.id } @@ -255,17 +263,81 @@ class Controller { return AfiPieChart(AfiPieChartDataset(monitoring, knowledge, control, conservatism, teamwork)) } - fun sendObservationToSocket(observation: Observation) { - if (::websocketMessenger.isInitialized) { - websocketMessenger.convertAndSend("/ws/observations", observation) - } else { - logger.warn("WebSocket messenger is not initialized. Not sending data to socket.") + @PostMapping("/grpob/start") + fun startGroupObservation(initData: GroupObservationInit): Map { + val site = siteRepository.findById(initData.site) + val tutors = tutorRepository.findAllById(initData.tutors).toSet() + if (!site.isPresent) { + logger.info("Attempted to add Observation without a site.") + return mapOf("error" to "Site required") + } + if (tutors.isEmpty() || tutors.size != initData.tutors.size) { + logger.info("Attempted to add Observation without a tutor") + return mapOf("error" to "Tutor(s) required") + } + val sessionId = GroupSessionManager.startNewSession(site.get(), tutors, initData.type, initData.scenarioTitles) + return getConnectionDetails().plus("id" to sessionId.toString()) + } + + @GetMapping("/grpob/recover") + fun reconnectToGroupObservation(): Map { + logger.debug("Previous group observation requested") + return getConnectionDetails().plus(mapOf("id" to GroupSessionManager.sessionId.toString(), "observations" to GroupSessionManager.observations)) + } + + @GetMapping("/grpob/valid/{id}") + fun checkGroupObservationValidityById(@PathVariable id: Int): Map { + if(GroupSessionManager.isValid(id)){ + return mapOf("titles" to GroupSessionManager.scenarioTitles!!) + } + logger.warn("Group observation requested with id $id but there is no valid session") + return mapOf("error" to "no valid session") + } + + @GetMapping("/grpob/valid") + fun checkGroupObservationValidity(): Boolean { + return GroupSessionManager.isValid() + } + + @PostMapping("/grpob/submit") + fun addGroupObservation(observationData: GroupObservation) { + if (GroupSessionManager.isValid()) { + var observation = Observation( + site = GroupSessionManager.site!!, + date = LocalDate.now(), + type = GroupSessionManager.trainingType!!, + observed = observationData.scenarios.joinToString { it.title }, + monitoring = observationData.scenarios.map { it.monitoring.rating }.average(), + conservatism = observationData.scenarios.map { it.conservatism.rating }.average(), + controlProcedural = observationData.scenarios.map { it.controlProcedural.rating }.average(), + control = observationData.scenarios.map { it.control.rating }.average(), + teamworkCommunications = observationData.scenarios.map { it.teamworkCommunications.rating }.average(), + teamworkLeadership = observationData.scenarios.map { it.teamworkLeadership.rating }.average(), + teamworkWorkload = observationData.scenarios.map { it.teamworkWorkload.rating }.average(), + knowledge = observationData.scenarios.map { it.knowledge.rating }.average(), + scenarios = observationData.scenarios, + tutors = GroupSessionManager.tutors!!, + person = personRepository.findFirstByNameLike(observationData.person.toUpperCase()) ?: personRepository.save(Person(name = observationData.person.toUpperCase())) + ) + observation = saveObservation(observation) + GroupSessionManager.addObservation(observation) } } @GetMapping("/address") fun getConnectionDetails(): Map { - return mapOf("ip" to InetAddress.getLocalHost().hostAddress) + var ipv4: String? = null + var retryCount = 0 + while (ipv4 == null && retryCount < 3) { + ipv4 = NetworkInterface.getNetworkInterfaces().asSequence() + .filter { !it.isLoopback }.map { x -> x.inetAddresses.asSequence() + .filter { it is Inet4Address }.map { it.hostAddress } }.flatten().firstOrNull() + retryCount++ + Thread.sleep(1_000) // Sleep for 1 second + } + if (ipv4 != null) + return mapOf("ip" to ipv4, "port" to environment["local.server.port"]) + return mapOf("error" to "Could not determine IP Address") } } diff --git a/backend/src/main/kotlin/uk/co/neviyn/observationdatabase/GroupSessionManager.kt b/backend/src/main/kotlin/uk/co/neviyn/observationdatabase/GroupSessionManager.kt new file mode 100644 index 0000000..61ade0d --- /dev/null +++ b/backend/src/main/kotlin/uk/co/neviyn/observationdatabase/GroupSessionManager.kt @@ -0,0 +1,62 @@ +package uk.co.neviyn.observationdatabase + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.stereotype.Service +import java.util.concurrent.ThreadLocalRandom + +@Service +object GroupSessionManager { + + private val logger: Logger = LoggerFactory.getLogger(javaClass)!! + + @Autowired + lateinit var websocketMessenger: SimpMessagingTemplate + + var sessionId = ThreadLocalRandom.current().nextInt(1000, 9999) + var site: Site? = null + var tutors: Set? = null + var trainingType: TrainingType? = null + var scenarioTitles: List? = null + var observations: MutableList = mutableListOf() + + fun startNewSession(site: Site, tutors: Set, trainingType: TrainingType, scenarioTitles: List): Int { + logger.info("Starting new Group Session") + logger.debug("Previous ID was $sessionId") + val prevId = sessionId + while (sessionId == prevId) { + this.sessionId = ThreadLocalRandom.current().nextInt(1000, 9999) + } + logger.debug("New ID is $sessionId") + this.site = site + this.tutors = tutors + this.trainingType = trainingType + this.scenarioTitles = scenarioTitles + observations = mutableListOf() + logger.debug("Group session initialized") + return sessionId + } + + fun isValid(sessionId: Int): Boolean { + return isValid() && sessionId == this.sessionId + } + + fun isValid(): Boolean { + return site != null && tutors != null && trainingType != null && scenarioTitles != null + } + + fun addObservation(observation: Observation) { + observations.add(observation) + sendObservationsToSocket() + } + + private fun sendObservationsToSocket() { + if (::websocketMessenger.isInitialized) { + websocketMessenger.convertAndSend("/ws/observations", mapOf("observations" to observations)) + } else { + logger.warn("WebSocket messenger is not initialized. Not sending data to socket.") + } + } +} \ No newline at end of file