diff --git a/src/main/scala/fi/oph/koski/massaluovutus/MassaluovutusResultWriter.scala b/src/main/scala/fi/oph/koski/massaluovutus/MassaluovutusResultWriter.scala index 8fad70ac3e..02d70b299a 100644 --- a/src/main/scala/fi/oph/koski/massaluovutus/MassaluovutusResultWriter.scala +++ b/src/main/scala/fi/oph/koski/massaluovutus/MassaluovutusResultWriter.scala @@ -7,6 +7,8 @@ import fi.oph.koski.koskiuser.KoskiSpecificSession import fi.oph.koski.localization.LocalizationReader import fi.oph.koski.raportit.{DataSheet, ExcelWriter, OppilaitosRaporttiResponse} import fi.oph.koski.util.CsvFormatter +import org.json4s.JValue +import org.json4s.jackson.JsonMethods import software.amazon.awssdk.core.internal.sync.FileContentStreamProvider import software.amazon.awssdk.http.ContentStreamProvider @@ -57,6 +59,9 @@ case class QueryResultWriter( def putJson[T: TypeTag](name: String, obj: T)(implicit user: KoskiSpecificSession): Unit = putJson(name, JsonSerializer.write(obj)) + def putJson(name: String, json: JValue): Unit = + putJson(name, JsonMethods.pretty(json)) + def createCsv[T <: Product](name: String, partitionSize: Option[Long])(implicit manager: Using.Manager): CsvStream[T] = manager(new CsvStream[T](s"$queryId-$name", partitionSize, { (file, partition) => results.putFile( diff --git a/src/main/scala/fi/oph/koski/massaluovutus/luokallejaaneet/MassaluovutusQueryLuokalleJaaneet.scala b/src/main/scala/fi/oph/koski/massaluovutus/luokallejaaneet/MassaluovutusQueryLuokalleJaaneet.scala new file mode 100644 index 0000000000..6a13192b52 --- /dev/null +++ b/src/main/scala/fi/oph/koski/massaluovutus/luokallejaaneet/MassaluovutusQueryLuokalleJaaneet.scala @@ -0,0 +1,115 @@ +package fi.oph.koski.massaluovutus.luokallejaaneet + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.JsonNodeFactory +import com.github.fge.jsonpatch.JsonPatch +import fi.oph.koski.config.KoskiApplication +import fi.oph.koski.db.PostgresDriverWithJsonSupport.plainAPI._ +import fi.oph.koski.db.{DB, DatabaseConverters, QueryMethods} +import fi.oph.koski.history.OpiskeluoikeusHistoryPatch +import fi.oph.koski.http.HttpStatus +import fi.oph.koski.json.JsonSerializer +import fi.oph.koski.koskiuser.{AccessType, KoskiSpecificSession, Rooli} +import fi.oph.koski.log.Logging +import fi.oph.koski.massaluovutus.MassaluovutusUtils.defaultOrganisaatio +import fi.oph.koski.massaluovutus.{MassaluovutusQueryParameters, QueryFormat, QueryResultWriter} +import fi.oph.koski.schema.annotation.EnumValues +import fi.oph.scalaschema.annotation.Description +import org.json4s.jackson.JsonMethods +import org.json4s.{JArray, JValue} + +case class MassaluovutusQueryLuokalleJaaneet ( + @EnumValues(Set("luokallejaaneet")) + `type`: String = "luokallejaaneet", + @EnumValues(Set(QueryFormat.json)) + format: String = QueryFormat.json, + @Description("Kyselyyn otettavan koulutustoimijan tai oppilaitoksen oid. Jos ei ole annettu, päätellään käyttäjän käyttöoikeuksista.") + organisaatioOid: Option[String], +) extends MassaluovutusQueryParameters with DatabaseConverters with Logging { + override def run(application: KoskiApplication, writer: QueryResultWriter)(implicit user: KoskiSpecificSession): Either[String, Unit] = { + val oppilaitosOids = application.organisaatioService.organisaationAlaisetOrganisaatiot(organisaatioOid.get) + val opiskeluoikeusOids = haeLuokalleJäämisenSisältävätOpiskeluoikeusOidit(application.raportointiDatabase.db, oppilaitosOids) + + logger.info(s"Opiskeluoikeus oids: $organisaatioOid -> ${opiskeluoikeusOids}") + + opiskeluoikeusOids.foreach { opiskeluoikeusOid => + application.historyRepository + .findByOpiskeluoikeusOid(opiskeluoikeusOid) + .map { patches => + patches + .foldLeft(LuokalleJääntiAccumulator()) { (acc, diff) => acc.next(diff) } + .matches + .foreach { case (luokka, oo) => + writer.putJson(s"${opiskeluoikeusOid}_luokka_$luokka", oo) + } + } + } + + Right(Unit) + } + + override def queryAllowed(application: KoskiApplication)(implicit user: KoskiSpecificSession): Boolean = + user.hasGlobalReadAccess || ( + organisaatioOid.exists(user.organisationOids(AccessType.read).contains) + && user.sensitiveDataAllowed(Set(Rooli.LUOTTAMUKSELLINEN_KAIKKI_TIEDOT)) + ) + + override def fillAndValidate(implicit user: KoskiSpecificSession): Either[HttpStatus, MassaluovutusQueryLuokalleJaaneet] = + if (organisaatioOid.isEmpty) { + defaultOrganisaatio.map(o => copy(organisaatioOid = Some(o))) + } else { + Right(this) + } + + private def haeLuokalleJäämisenSisältävätOpiskeluoikeusOidit(raportointiDb: DB, oppilaitosOids: Seq[String]): Seq[String] = + QueryMethods.runDbSync(raportointiDb, sql""" + SELECT DISTINCT r_opiskeluoikeus.opiskeluoikeus_oid + FROM r_opiskeluoikeus + JOIN r_paatason_suoritus ON r_paatason_suoritus.opiskeluoikeus_oid = r_opiskeluoikeus.opiskeluoikeus_oid + WHERE koulutusmuoto = 'perusopetus' + AND jaa_luokalle + AND oppilaitos_oid = any($oppilaitosOids) + """.as[String]) +} + +case class LuokalleJääntiAccumulator( + opiskeluoikeus: JsonNode = JsonNodeFactory.instance.objectNode(), + invalidHistory: Boolean = false, + matches: Map[String, JValue] = Map(), +) { + def next(diff: OpiskeluoikeusHistoryPatch): LuokalleJääntiAccumulator = { + try { + val oo = JsonPatch.fromJson(JsonMethods.asJsonNode(diff.muutos)).apply(opiskeluoikeus) + LuokalleJääntiAccumulator( + oo, + invalidHistory, + newMathes(oo), + ) + } catch { + case _: Exception => LuokalleJääntiAccumulator(invalidHistory = true) + } + + } + + private def newMathes(oo: JsonNode): Map[String, JValue] = + jääLuokalleLuokilla(oo).foldLeft(Map() : Map[String, JValue]) { (acc, m) => + val (luokka, ooJson) = m + if (acc.keySet.contains(luokka)) { + acc + } else { + acc + (luokka -> ooJson) + } + } + + private def jääLuokalleLuokilla(oo: JsonNode): List[(String, JValue)] = + suoritukset(oo) + .arr + .filter { s => JsonSerializer.extract[Option[Boolean]](s \ "jääLuokalle").getOrElse(false) } + .map { s => ( + JsonSerializer.extract[String](s \ "koulutusmoduuli" \ "tunniste" \ "koodiarvo"), + s + ) } + + private def suoritukset(oo: JsonNode): JArray = (JsonMethods.fromJsonNode(oo) \ "suoritukset").asInstanceOf[JArray] +} +