Skip to content

Commit

Permalink
Luokalle jääneiden massaluovutuskysely
Browse files Browse the repository at this point in the history
  • Loading branch information
ilkkahanninen committed Jan 20, 2025
1 parent 350e99c commit b70d34d
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,13 @@ Esimerkki laskentataulukkomuotoisen datan hakemisesta:
{{var:headers}}

{{json:PaallekkaisetOpiskeluoikeudetXlsx}}

{{title:fi.oph.koski.massaluovutus.luokallejaaneet.MassaluovutusQueryLuokalleJaaneet}}
{{docs:fi.oph.koski.massaluovutus.luokallejaaneet.MassaluovutusQueryLuokalleJaaneet}}

Esimerkki:

POST {{var:baseUrl}}/api/massaluovutus HTTP/1.1
{{var:headers}}

{{json:LuokalleJaaneetJson}}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import fi.oph.koski.kela.KelaSchema
import fi.oph.koski.koodisto.Koodistot
import fi.oph.koski.koskiuser.Unauthenticated
import fi.oph.koski.luovutuspalvelu.HslResponse
import fi.oph.koski.massaluovutus.luokallejaaneet.MassaluovutusQueryLuokalleJaaneetResult
import fi.oph.koski.massaluovutus.suoritusrekisteri.SureResponse
import fi.oph.koski.massaluovutus.valintalaskenta.ValintalaskentaResult
import fi.oph.koski.migri.MigriSchema
Expand Down Expand Up @@ -129,6 +130,10 @@ class DocumentationApiServlet(application: KoskiApplication) extends KoskiSpecif
SureResponse.schemaJson
}

get("/luokalle-jaaneet-result.json") {
MassaluovutusQueryLuokalleJaaneetResult.schemaJson
}

get("/omadata-oauth2-suoritetut-tutkinnot-oppija-schema.json") {
OmaDataOAuth2SuoritetutTutkinnot.schemaJson
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import fi.oph.koski.config.KoskiApplication
import fi.oph.koski.documentation.Markdown
import fi.oph.koski.json.JsonSerializer
import fi.oph.koski.log.Logging
import fi.oph.koski.massaluovutus.luokallejaaneet.MassaluovutusQueryLuokalleJaaneetExamples
import fi.oph.koski.massaluovutus.organisaationopiskeluoikeudet.{QueryOrganisaationOpiskeluoikeudetCsvDocumentation, QueryOrganisaationOpiskeluoikeudetJsonDocumentation}
import fi.oph.koski.massaluovutus.paallekkaisetopiskeluoikeudet.QueryPaallekkaisetOpiskeluoikeudetDocumentation
import fi.oph.koski.massaluovutus.valintalaskenta.ValintalaskentaQueryDocumentation
Expand Down Expand Up @@ -237,6 +238,7 @@ object QueryExamples {
ValintalaskentaQueryDocumentation.outputFiles,
application.config.getString("koski.root.url"),
))
case "LuokalleJaaneetJson" => asJson(MassaluovutusQueryLuokalleJaaneetExamples.query)
case _ => None
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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.{KoskeenTallennettavaOpiskeluoikeus, PerusopetuksenOpiskeluoikeus}
import fi.oph.koski.schema.annotation.EnumValues
import fi.oph.scalaschema.annotation.{Description, Title}
import org.json4s.jackson.JsonMethods
import org.json4s.{JArray, JValue}

import java.sql.Timestamp

@Title("Perusopetuksen luokalle jäämiset")
@Description("Tämä kysely on tarkoitettu opiskeluoikeusversioiden löytäiseksi KOSKI-varannoksi, joissa perusopetuksen opiskeluoikeuteen on merkitty tieto, että oppilas jää luokalle.")
@Description("Vastauksen skeema on saatavana <a href=\"/koski/json-schema-viewer/?schema=luokalle-jaaneet-result.json\">täältä.</a>")
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 oids = haeLuokalleJäämisenSisältävätOpiskeluoikeusOidit(application.raportointiDatabase.db, oppilaitosOids)

oids.foreach { case (oppijaOid, opiskeluoikeusOid) =>
application.historyRepository
.findByOpiskeluoikeusOid(opiskeluoikeusOid)
.map { patches =>
patches
.foldLeft(LuokalleJääntiAccumulator()) { (acc, diff) => acc.next(diff) }
.matches
.foreach { case (luokka, oo) =>
val response = MassaluovutusQueryLuokalleJaaneetResult(oo, luokka, oppijaOid)
writer.putJson(s"${opiskeluoikeusOid}_luokka_$luokka", response)
}
}
}

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, String)] =
QueryMethods.runDbSync(raportointiDb, sql"""
SELECT
r_opiskeluoikeus.oppija_oid,
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)
GROUP BY
r_opiskeluoikeus.oppija_oid,
r_opiskeluoikeus.opiskeluoikeus_oid
""".as[Tuple2[String, String]])
}

case class LuokalleJääntiAccumulator(
opiskeluoikeus: JsonNode = JsonNodeFactory.instance.objectNode(),
invalidHistory: Boolean = false,
matches: Map[String, LuokalleJääntiMatch] = Map(),
) {
def next(diff: OpiskeluoikeusHistoryPatch): LuokalleJääntiAccumulator = {
try {
val oo = JsonPatch.fromJson(JsonMethods.asJsonNode(diff.muutos)).apply(opiskeluoikeus)
LuokalleJääntiAccumulator(
oo,
invalidHistory,
newMathes(oo, diff),
)
} catch {
case _: Exception => LuokalleJääntiAccumulator(invalidHistory = true)
}

}

private def newMathes(oo: JsonNode, diff: OpiskeluoikeusHistoryPatch): Map[String, LuokalleJääntiMatch] =
jääLuokalleLuokilla(oo).foldLeft(Map() : Map[String, LuokalleJääntiMatch]) { (acc, m) =>
val (luokka, ooJson) = m
if (acc.keySet.contains(luokka)) {
acc
} else {
acc + (luokka -> LuokalleJääntiMatch(ooJson, diff))
}
}

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"),
JsonMethods.fromJsonNode(oo),
) }

private def suoritukset(oo: JsonNode): JArray = (JsonMethods.fromJsonNode(oo) \ "suoritukset").asInstanceOf[JArray]
}

case class LuokalleJääntiMatch(
opiskeluoikeus: JValue,
aikaleima: Timestamp,
versio: Int,
) {
def perusopetuksenOpiskeluoikeus: PerusopetuksenOpiskeluoikeus =
JsonSerializer
.extract[PerusopetuksenOpiskeluoikeus](opiskeluoikeus)
.copy(
versionumero = Some(versio),
aikaleima = Some(aikaleima.toLocalDateTime),
)
}

object LuokalleJääntiMatch {
def apply(opiskeluoikeus: JValue, diff: OpiskeluoikeusHistoryPatch): LuokalleJääntiMatch = LuokalleJääntiMatch(
opiskeluoikeus = opiskeluoikeus,
aikaleima = diff.aikaleima,
versio = diff.versionumero,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package fi.oph.koski.massaluovutus.luokallejaaneet

import fi.oph.koski.organisaatio.MockOrganisaatiot

object MassaluovutusQueryLuokalleJaaneetExamples {
val query: MassaluovutusQueryLuokalleJaaneet = MassaluovutusQueryLuokalleJaaneet(
organisaatioOid = Some(MockOrganisaatiot.helsinginKaupunki)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package fi.oph.koski.massaluovutus.luokallejaaneet

import fi.oph.koski.schema.{KoskiSchema, PerusopetuksenOpiskeluoikeus}
import fi.oph.scalaschema.{ClassSchema, SchemaToJson}
import fi.oph.scalaschema.annotation.{Description, Title}
import org.json4s.JValue

import java.time.LocalDateTime

@Title("Luokalle jääneet -kyselyn vastaus")
case class MassaluovutusQueryLuokalleJaaneetResult(
@Description("Oppijan oid")
oppijaOid: String,
@Description("Luokka-aste, jolle luokalle jääminen on merkitty")
luokka: String,
@Description("Opiskeluoikeuden tiedot")
opiskeluoikeus: PerusopetuksenOpiskeluoikeus,
)

object MassaluovutusQueryLuokalleJaaneetResult {
def apply(m: LuokalleJääntiMatch, luokka: String, oppijaOid: String): MassaluovutusQueryLuokalleJaaneetResult =
MassaluovutusQueryLuokalleJaaneetResult(
oppijaOid = oppijaOid,
luokka = luokka,
opiskeluoikeus = m.perusopetuksenOpiskeluoikeus,
)

lazy val schemaJson: JValue =
SchemaToJson.toJsonSchema(KoskiSchema.createSchema(classOf[MassaluovutusQueryLuokalleJaaneetResult]).asInstanceOf[ClassSchema])
}

0 comments on commit b70d34d

Please sign in to comment.