From 25066c5ed78aca6015d60cc77f7f913eb1876327 Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Thu, 1 Jul 2021 00:24:37 +0100 Subject: [PATCH 01/67] Correction to RawAcademicCalendar parsing. --- .../integration/domain/calendar/BusinessObjects.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/domain/calendar/BusinessObjects.kt b/src/main/kotlin/org/ionproject/integration/domain/calendar/BusinessObjects.kt index 0e1ce052..056d5d18 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/calendar/BusinessObjects.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/calendar/BusinessObjects.kt @@ -60,11 +60,11 @@ data class AcademicCalendar( descriptions.forEachIndexed { index, _ -> val intervalDate = DateUtils.getDateRange(dates[index].value) - when (getEventType(events[index])) { + when (getEventType(descriptions[index].value)) { EventType.EVALUATION -> { evaluations.add( Evaluation( - events[index], + descriptions[index].value, intervalDate.from, intervalDate.to, false @@ -74,7 +74,7 @@ data class AcademicCalendar( EventType.INTERRUPTION -> { interruptions.add( Event( - events[index], + descriptions[index].value, intervalDate.from, intervalDate.to, ) @@ -83,7 +83,7 @@ data class AcademicCalendar( EventType.DETAILS -> { details.add( Detail( - events[index], + descriptions[index].value, listOf(), intervalDate.from, intervalDate.to, @@ -93,7 +93,7 @@ data class AcademicCalendar( EventType.OTHER -> { lectures.add( Event( - events[index], + descriptions[index].value, intervalDate.from, intervalDate.to, ) From 4a2af01470dc16e8b969eeb6d9651a061765eb66 Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Fri, 2 Jul 2021 23:14:58 +0100 Subject: [PATCH 02/67] Academic Calendar Business Object Unitary test completed. --- ...micCalendarBusinessObjFormatCheckerTest.kt | 21 -- ...icCalendarBusinessObjFormatCheckerTests.kt | 187 ++++++++++++++++++ 2 files changed, 187 insertions(+), 21 deletions(-) delete mode 100644 src/test/kotlin/org/ionproject/integration/format/implementations/AcademicCalendarBusinessObjFormatCheckerTest.kt create mode 100644 src/test/kotlin/org/ionproject/integration/format/implementations/AcademicCalendarBusinessObjFormatCheckerTests.kt diff --git a/src/test/kotlin/org/ionproject/integration/format/implementations/AcademicCalendarBusinessObjFormatCheckerTest.kt b/src/test/kotlin/org/ionproject/integration/format/implementations/AcademicCalendarBusinessObjFormatCheckerTest.kt deleted file mode 100644 index 2301bed0..00000000 --- a/src/test/kotlin/org/ionproject/integration/format/implementations/AcademicCalendarBusinessObjFormatCheckerTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.ionproject.integration.format.implementations - -import org.ionproject.integration.domain.calendar.RawCalendarData -import org.junit.jupiter.api.Test - -internal class AcademicCalendarBusinessObjFormatCheckerTest { - - private val tableData = - """[{"extraction_method":"lattice","top":300.81226,"left":56.926014,"width":479.52911376953125,"height":183.77505493164062,"right":536.45514,"bottom":484.5873,"data":[[{"top":300.81226,"left":56.926014,"width":247.92564392089844,"height":15.077880859375,"text":"Divulgação de horários"},{"top":300.81226,"left":304.85165,"width":231.60348510742188,"height":15.077880859375,"text":"9 de setembro de 2020"}],[{"top":315.89014,"left":56.926014,"width":247.92564392089844,"height":15.23968505859375,"text":"Abertura das atividades letivas 2020/2021"},{"top":315.89014,"left":304.85165,"width":231.60348510742188,"height":15.23968505859375,"text":"28 de setembro de 2020"}],[{"top":331.12982,"left":56.926014,"width":247.92564392089844,"height":15.48004150390625,"text":"Início das aulas"},{"top":331.12982,"left":304.85165,"width":231.60348510742188,"height":15.48004150390625,"text":"6 de outubro de 2020"}],[{"top":346.60986,"left":56.926014,"width":247.92564392089844,"height":15.120269775390625,"text":"Fim das aulas"},{"top":346.60986,"left":304.85165,"width":231.60348510742188,"height":15.120269775390625,"text":"23 de janeiro de 2021"}],[{"top":361.73013,"left":56.926014,"width":247.92564392089844,"height":29.760101318359375,"text":"Interrupção de atividades letivas (Natal)"},{"top":361.73013,"left":304.85165,"width":231.60348510742188,"height":29.760101318359375,"text":"21 de dezembro de 2020 a 3 de janeiro de\r2021"}],[{"top":391.49023,"left":56.926014,"width":247.92564392089844,"height":15.239990234375,"text":"Período de exames (época normal)"},{"top":391.49023,"left":304.85165,"width":231.60348510742188,"height":15.239990234375,"text":"25 de janeiro a 13 de fevereiro de 2021"}],[{"top":406.73022,"left":56.926014,"width":247.92564392089844,"height":17.879638671875,"text":"Interrupção de atividades letivas (Carnaval)"},{"top":406.73022,"left":304.85165,"width":231.60348510742188,"height":17.879638671875,"text":"15 e 16 de fevereiro de 2021"}],[{"top":424.60986,"left":56.926014,"width":247.92564392089844,"height":15.11968994140625,"text":"Período de exames (época de recurso)"},{"top":424.60986,"left":304.85165,"width":231.60348510742188,"height":15.11968994140625,"text":"17 de fevereiro a 2 de março de 2021"}],[{"top":439.72955,"left":56.926014,"width":247.92564392089844,"height":44.857757568359375,"text":"Data limite para lançamento de classificações no\rPortal Académico (frequência, exames de época\rnormal e de época de recurso)"},{"top":439.72955,"left":304.85165,"width":231.60348510742188,"height":44.857757568359375,"text":"9 de março de 2021"}]]},{"extraction_method":"lattice","top":531.5918,"left":56.926014,"width":481.6865234375,"height":210.5386962890625,"right":538.61255,"bottom":742.1305,"data":[[{"top":531.5918,"left":56.926014,"width":247.92564392089844,"height":15.07818603515625,"text":"Divulgação de horários"},{"top":531.5918,"left":304.85165,"width":233.76089477539062,"height":15.07818603515625,"text":"22 de fevereiro de 2021"}],[{"top":546.67,"left":56.926014,"width":247.92564392089844,"height":15.11981201171875,"text":"Início das aulas"},{"top":546.67,"left":304.85165,"width":233.76089477539062,"height":15.11981201171875,"text":"15 de março de 2021"}],[{"top":561.7898,"left":56.926014,"width":247.92564392089844,"height":15.23992919921875,"text":"Interrupção de atividades letivas (Páscoa)"},{"top":561.7898,"left":304.85165,"width":233.76089477539062,"height":15.23992919921875,"text":"29 de março a 5 de abril de 2021"}],[{"top":577.0297,"left":56.926014,"width":247.92564392089844,"height":15.12060546875,"text":"Fim das aulas"},{"top":577.0297,"left":304.85165,"width":233.76089477539062,"height":15.12060546875,"text":"26 de junho de 2021"}],[{"top":592.1503,"left":56.926014,"width":247.92564392089844,"height":15.11981201171875,"text":"Período de exames (época normal)"},{"top":592.1503,"left":304.85165,"width":233.76089477539062,"height":15.11981201171875,"text":"28 de junho a 17 de julho de 2021"}],[{"top":607.27014,"left":56.926014,"width":247.92564392089844,"height":15.1214599609375,"text":"Período de exames (época de recurso)"},{"top":607.27014,"left":304.85165,"width":233.76089477539062,"height":15.1214599609375,"text":"19 a 31 de julho de 2021"}],[{"top":622.3916,"left":56.926014,"width":247.92564392089844,"height":44.5482177734375,"text":"Data limite para lançamento de classificações no\rPortal Académico (frequência, exames de época\rnormal e de época de recurso)"},{"top":622.3916,"left":304.85165,"width":233.76089477539062,"height":44.5482177734375,"text":"2 de setembro de 2021"}],[{"top":666.9398,"left":56.926014,"width":247.92564392089844,"height":15.12060546875,"text":"Encerramento das atividades letivas 2020/2021"},{"top":666.9398,"left":304.85165,"width":233.76089477539062,"height":15.12060546875,"text":"31 de julho de 2021"}],[{"top":682.0604,"left":56.926014,"width":247.92564392089844,"height":15.12005615234375,"text":"Período de ausência de atividade letiva (férias)"},{"top":682.0604,"left":304.85165,"width":233.76089477539062,"height":15.12005615234375,"text":"1 a 31 de agosto de 2021"}],[{"top":697.1805,"left":56.926014,"width":247.92564392089844,"height":15.11834716796875,"text":"Exames de época especial"},{"top":697.1805,"left":304.85165,"width":233.76089477539062,"height":15.11834716796875,"text":"2 a 18 de setembro de 2021"}],[{"top":712.2988,"left":56.926014,"width":247.92564392089844,"height":29.8316650390625,"text":"Data limite para lançamento de classificações no\rPortal Académico (época especial)"},{"top":712.2988,"left":304.85165,"width":233.76089477539062,"height":29.8316650390625,"text":"30 de setembro de 2021"}]]},{"extraction_method":"lattice","top":106.02233,"left":56.923706,"width":481.68896484375,"height":74.67122650146484,"right":538.6127,"bottom":180.69356,"data":[[{"top":106.02233,"left":56.923706,"width":247.92626953125,"height":29.83771514892578,"text":"Data limite para entrega de trabalhos finais de\rlicenciatura*"},{"top":106.02233,"left":304.84998,"width":233.7626953125,"height":29.83771514892578,"text":"30 de setembro de 2021"}],[{"top":135.86005,"left":56.923706,"width":247.92626953125,"height":29.759963989257812,"text":"Data limite para entrega de trabalhos finais de\rmestrado"},{"top":135.86005,"left":304.84998,"width":233.7626953125,"height":29.759963989257812,"text":"30 de setembro de 2021"}],[{"top":165.62001,"left":56.923706,"width":247.92626953125,"height":15.07354736328125,"text":"Fim do ano letivo 2020/2021"},{"top":165.62001,"left":304.84998,"width":233.7626953125,"height":15.07354736328125,"text":"30 de setembro de 2021"}]]}]""" - private val textData = listOf() - private val validUTCDate = "20210516T214838Z" - - @Test - fun draftTest() { - val calendarData = RawCalendarData( - textData, - tableData, - validUTCDate - ) - } -} diff --git a/src/test/kotlin/org/ionproject/integration/format/implementations/AcademicCalendarBusinessObjFormatCheckerTests.kt b/src/test/kotlin/org/ionproject/integration/format/implementations/AcademicCalendarBusinessObjFormatCheckerTests.kt new file mode 100644 index 00000000..1af493a1 --- /dev/null +++ b/src/test/kotlin/org/ionproject/integration/format/implementations/AcademicCalendarBusinessObjFormatCheckerTests.kt @@ -0,0 +1,187 @@ +package org.ionproject.integration.format.implementations + +import org.ionproject.integration.application.config.AppProperties +import org.ionproject.integration.application.dispatcher.IDispatcher +import org.ionproject.integration.application.job.ISELAcademicCalendarJob +import org.ionproject.integration.domain.common.Language +import org.ionproject.integration.domain.common.School +import org.ionproject.integration.infrastructure.http.IFileDownloader +import org.ionproject.integration.model.external.calendar.AcademicCalendar +import org.ionproject.integration.model.external.calendar.Detail +import org.ionproject.integration.model.external.calendar.Evaluation +import org.ionproject.integration.model.external.calendar.Event +import org.ionproject.integration.model.external.calendar.Term +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.mockito.kotlin.mock +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory +import java.io.File +import java.time.LocalDate +import java.time.Month +import javax.sql.DataSource + +class AcademicCalendarBusinessObjFormatCheckerTests { + private val mockJobBuilderFactory = mock {} + + private val mockStepBuilderFactory = mock {} + + private val mockAppProperties = mock {} + + private val mockDownloader = mock {} + + private val mockDataSource = mock {} + + private val mockDispatcher = mock {} + + @Test + fun `when given a academic calendar if business object extraction is as expected then success`() { + val resourceFile = File("src/test/resources/calendarTest.pdf") + + val job = ISELAcademicCalendarJob( + mockJobBuilderFactory, + mockStepBuilderFactory, + mockAppProperties, + mockDownloader, + mockDispatcher, + mockDataSource + ) + val calendarData = job.extractCalendarPDF(resourceFile.toPath().toString()) + + val academicCalendar = AcademicCalendar.from(calendarData) + val academicCalendarBO = AcademicCalendar( + "2020-07-06T16:00:21Z", + academicCalendar.retrievalDateTime, + School("Instituto Superior de Engenharia de Lisboa", "ISEL"), + Language.PT, + listOf( + Term( + "2020-2021-1", + listOf( + Event( + "Interrupção de atividades letivas (Natal)", + LocalDate.of(2020, Month.DECEMBER, 21), + LocalDate.of(2021, Month.JANUARY, 3) + ), + Event( + "Interrupção de atividades letivas (Carnaval)", + LocalDate.of(2021, Month.FEBRUARY, 15), + LocalDate.of(2021, Month.FEBRUARY, 16) + ) + ), + listOf( + Evaluation( + "Período de exames (época normal)", + LocalDate.of(2021, Month.JANUARY, 25), + LocalDate.of(2021, Month.FEBRUARY, 13), + false + ), + Evaluation( + "Período de exames (época de recurso)", + LocalDate.of(2021, Month.FEBRUARY, 17), + LocalDate.of(2021, Month.MARCH, 2), + false + ) + ), + listOf( + Detail( + "Aulas", + listOf(1, 2, 3, 4, 5, 6), + LocalDate.of(2020, Month.OCTOBER, 6), + LocalDate.of(2021, Month.JANUARY, 23) + ) + ), + listOf( + Event( + "Divulgação de horários", + LocalDate.of(2020, Month.SEPTEMBER, 9), + LocalDate.of(2020, Month.SEPTEMBER, 9) + ), + Event( + "Data limite para lançamento de classificações no Portal Académico (frequência, exames de época normal e de época de recurso)", + LocalDate.of(2021, Month.MARCH, 9), + LocalDate.of(2021, Month.MARCH, 9) + ) + ) + ), + Term( + "2020-2021-2", + listOf( + Event( + "Interrupção de atividades letivas (Páscoa)", + LocalDate.of(2021, Month.MARCH, 29), + LocalDate.of(2021, Month.APRIL, 5) + ), + Event( + "Período de ausência de atividade letiva (férias)", + LocalDate.of(2021, Month.AUGUST, 1), + LocalDate.of(2021, Month.AUGUST, 31) + ) + ), + listOf( + Evaluation( + "Período de exames (época normal)", + LocalDate.of(2021, Month.JUNE, 28), + LocalDate.of(2021, Month.JULY, 17), + false + ), + Evaluation( + "Período de exames (época de recurso)", + LocalDate.of(2021, Month.JULY, 19), + LocalDate.of(2021, Month.JULY, 31), + false + ), + Evaluation( + "Exames de época especial", + LocalDate.of(2021, Month.SEPTEMBER, 2), + LocalDate.of(2021, Month.SEPTEMBER, 18), + false + ) + ), + listOf( + Detail( + "Aulas", + listOf(1, 2, 3, 4, 5, 6), + LocalDate.of(2021, Month.MARCH, 15), + LocalDate.of(2021, Month.JUNE, 26) + ) + ), + listOf( + Event( + "Divulgação de horários", + LocalDate.of(2021, Month.FEBRUARY, 22), + LocalDate.of(2021, Month.FEBRUARY, 22) + ), + Event( + "Encerramento das atividades letivas 2020/2021", + LocalDate.of(2021, Month.JULY, 31), + LocalDate.of(2021, Month.JULY, 31) + ), + Event( + "Data limite para lançamento de classificações no Portal Académico (época especial)", + LocalDate.of(2021, Month.SEPTEMBER, 30), + LocalDate.of(2021, Month.SEPTEMBER, 30) + ), + Event( + "Data limite para entrega de trabalhos finais de licenciatura*", + LocalDate.of(2021, Month.SEPTEMBER, 30), + LocalDate.of(2021, Month.SEPTEMBER, 30) + ), + Event( + "Data limite para entrega de trabalhos finais de mestrado", + LocalDate.of(2021, Month.SEPTEMBER, 30), + LocalDate.of(2021, Month.SEPTEMBER, 30) + ), + Event( + "Fim do ano letivo 2020/2021", + LocalDate.of(2021, Month.SEPTEMBER, 30), + LocalDate.of(2021, Month.SEPTEMBER, 30) + ) + ) + ) + ) + ) + + assertEquals(academicCalendarBO, academicCalendar) + } +} From 90d9513843d0c9f1ec5f42064c58dd45fa6c81b8 Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Sat, 3 Jul 2021 00:10:24 +0100 Subject: [PATCH 03/67] Renaming Details to Lectures and ensuring variable declaration consistency. --- .../job/ISELAcademicCalendarJob.kt | 2 +- .../domain/calendar/BusinessObjects.kt | 28 +++++++++---------- .../domain/calendar/OutputRepresentations.kt | 16 +++++------ ...icCalendarBusinessObjFormatCheckerTests.kt | 26 +++++++++++------ 4 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt b/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt index 601cb695..fa4220ff 100644 --- a/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt +++ b/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt @@ -6,6 +6,7 @@ import org.ionproject.integration.application.dispatcher.DispatchResult import org.ionproject.integration.application.dispatcher.IDispatcher import org.ionproject.integration.application.dto.AcademicCalendarData import org.ionproject.integration.application.job.tasklet.DownloadAndCompareTasklet +import org.ionproject.integration.domain.calendar.AcademicCalendarDto import org.ionproject.integration.domain.calendar.RawCalendarData import org.ionproject.integration.infrastructure.Try import org.ionproject.integration.infrastructure.file.FileComparatorImpl @@ -18,7 +19,6 @@ import org.ionproject.integration.infrastructure.pdfextractor.AcademicCalendarEx import org.ionproject.integration.infrastructure.pdfextractor.ITextPdfExtractor import org.ionproject.integration.infrastructure.pdfextractor.PDFBytesFormatChecker import org.ionproject.integration.model.external.calendar.AcademicCalendar -import org.ionproject.integration.domain.calendar.AcademicCalendarDto import org.springframework.batch.core.ExitStatus import org.springframework.batch.core.configuration.annotation.JobBuilderFactory import org.springframework.batch.core.configuration.annotation.StepBuilderFactory diff --git a/src/main/kotlin/org/ionproject/integration/domain/calendar/BusinessObjects.kt b/src/main/kotlin/org/ionproject/integration/domain/calendar/BusinessObjects.kt index 056d5d18..9786cc56 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/calendar/BusinessObjects.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/calendar/BusinessObjects.kt @@ -21,7 +21,7 @@ data class AcademicCalendar( private const val TABLE_DELIMITER = "extraction" private const val PT_INTERRUPTION_REGEX = "\\b(?:Interrupção|Férias)\\b" private const val PT_EVALUATION_REGEX = "\\b(?:Exames|Testes)\\b" - private const val PT_DETAILS_REGEX = "\\b(?:Turmas)\\b" + private const val PT_LECTURES_REGEX = "\\b(?:Turmas)\\b" fun from(rawCalendarData: RawCalendarData): AcademicCalendar = AcademicCalendar( @@ -53,8 +53,8 @@ data class AcademicCalendar( private fun buildTerm(events: List, pdfRawText: String, term: CalendarTerm): Term { val interruptions = mutableListOf() val evaluations = mutableListOf() - val details = mutableListOf() - val lectures = mutableListOf() + val lectures = mutableListOf() + val otherEvents = mutableListOf() val (descriptions, dates) = events.withIndex().partition { it.index % 2 == 0 } descriptions.forEachIndexed { index, _ -> @@ -80,9 +80,9 @@ data class AcademicCalendar( ) ) } - EventType.DETAILS -> { - details.add( - Detail( + EventType.LECTURES -> { + lectures.add( + Lectures( descriptions[index].value, listOf(), intervalDate.from, @@ -91,7 +91,7 @@ data class AcademicCalendar( ) } EventType.OTHER -> { - lectures.add( + otherEvents.add( Event( descriptions[index].value, intervalDate.from, @@ -110,8 +110,8 @@ data class AcademicCalendar( .plus("-${getTermNumber(term)}"), interruptions, evaluations, - details, - lectures + lectures, + otherEvents ) } @@ -125,7 +125,7 @@ data class AcademicCalendar( return when { event.contains(PT_INTERRUPTION_REGEX.toRegex(RegexOption.IGNORE_CASE)) -> EventType.INTERRUPTION event.contains(PT_EVALUATION_REGEX.toRegex(RegexOption.IGNORE_CASE)) -> EventType.EVALUATION - event.contains(PT_DETAILS_REGEX.toRegex(RegexOption.IGNORE_CASE)) -> EventType.DETAILS + event.contains(PT_LECTURES_REGEX.toRegex(RegexOption.IGNORE_CASE)) -> EventType.LECTURES else -> EventType.OTHER } } @@ -143,8 +143,8 @@ data class Term( val calendarTerm: String = "", val interruptions: List, val evaluations: List, - val details: List, - val lectures: List + val lectures: List, + val otherEvents: List ) data class Event( @@ -160,7 +160,7 @@ data class Evaluation( val duringLectures: Boolean ) -data class Detail( +data class Lectures( val name: String, val curricularTerm: List, val startDate: LocalDate, @@ -175,6 +175,6 @@ enum class CalendarTerm { enum class EventType { INTERRUPTION, EVALUATION, - DETAILS, + LECTURES, OTHER } diff --git a/src/main/kotlin/org/ionproject/integration/domain/calendar/OutputRepresentations.kt b/src/main/kotlin/org/ionproject/integration/domain/calendar/OutputRepresentations.kt index d072e6e5..7247cdc9 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/calendar/OutputRepresentations.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/calendar/OutputRepresentations.kt @@ -3,7 +3,7 @@ package org.ionproject.integration.domain.calendar import org.ionproject.integration.domain.timetable.dto.SchoolDto import org.ionproject.integration.infrastructure.DateUtils import org.ionproject.integration.model.external.calendar.AcademicCalendar -import org.ionproject.integration.model.external.calendar.Detail +import org.ionproject.integration.model.external.calendar.Lectures import org.ionproject.integration.model.external.calendar.Evaluation import org.ionproject.integration.model.external.calendar.Event import org.ionproject.integration.model.external.calendar.Term @@ -32,8 +32,8 @@ data class TermDto( val calendarTerm: String, val interruptions: List, val evaluations: List, - val details: List, - val lectures: List + val lectures: List, + val otherEvents: List ) { companion object { fun from(terms: List): List = terms.map { @@ -41,8 +41,8 @@ data class TermDto( it.calendarTerm, EventDto.from(it.interruptions), EvaluationDto.from(it.evaluations), - DetailDto.from(it.details), - EventDto.from(it.lectures) + LecturesDto.from(it.lectures), + EventDto.from(it.otherEvents) ) } } @@ -82,15 +82,15 @@ data class EvaluationDto( } } -data class DetailDto( +data class LecturesDto( val name: String, val curricularTerm: List, val startDate: String, val endDate: String, ) { companion object { - fun from(details: List): List = details.map { - DetailDto( + fun from(lectures: List): List = lectures.map { + LecturesDto( it.name, it.curricularTerm.map { id -> IdDto(id) }, DateUtils.formatToCalendarDate(it.startDate), diff --git a/src/test/kotlin/org/ionproject/integration/format/implementations/AcademicCalendarBusinessObjFormatCheckerTests.kt b/src/test/kotlin/org/ionproject/integration/format/implementations/AcademicCalendarBusinessObjFormatCheckerTests.kt index 1af493a1..99ff5cc7 100644 --- a/src/test/kotlin/org/ionproject/integration/format/implementations/AcademicCalendarBusinessObjFormatCheckerTests.kt +++ b/src/test/kotlin/org/ionproject/integration/format/implementations/AcademicCalendarBusinessObjFormatCheckerTests.kt @@ -7,7 +7,7 @@ import org.ionproject.integration.domain.common.Language import org.ionproject.integration.domain.common.School import org.ionproject.integration.infrastructure.http.IFileDownloader import org.ionproject.integration.model.external.calendar.AcademicCalendar -import org.ionproject.integration.model.external.calendar.Detail +import org.ionproject.integration.model.external.calendar.Lectures import org.ionproject.integration.model.external.calendar.Evaluation import org.ionproject.integration.model.external.calendar.Event import org.ionproject.integration.model.external.calendar.Term @@ -84,7 +84,7 @@ class AcademicCalendarBusinessObjFormatCheckerTests { ) ), listOf( - Detail( + Lectures( "Aulas", listOf(1, 2, 3, 4, 5, 6), LocalDate.of(2020, Month.OCTOBER, 6), @@ -97,6 +97,11 @@ class AcademicCalendarBusinessObjFormatCheckerTests { LocalDate.of(2020, Month.SEPTEMBER, 9), LocalDate.of(2020, Month.SEPTEMBER, 9) ), + Event( + "Abertura das atividades letivas 2020/2021", + LocalDate.of(2020, Month.SEPTEMBER, 28), + LocalDate.of(2020, Month.SEPTEMBER, 28) + ), Event( "Data limite para lançamento de classificações no Portal Académico (frequência, exames de época normal e de época de recurso)", LocalDate.of(2021, Month.MARCH, 9), @@ -111,11 +116,6 @@ class AcademicCalendarBusinessObjFormatCheckerTests { "Interrupção de atividades letivas (Páscoa)", LocalDate.of(2021, Month.MARCH, 29), LocalDate.of(2021, Month.APRIL, 5) - ), - Event( - "Período de ausência de atividade letiva (férias)", - LocalDate.of(2021, Month.AUGUST, 1), - LocalDate.of(2021, Month.AUGUST, 31) ) ), listOf( @@ -139,7 +139,7 @@ class AcademicCalendarBusinessObjFormatCheckerTests { ) ), listOf( - Detail( + Lectures( "Aulas", listOf(1, 2, 3, 4, 5, 6), LocalDate.of(2021, Month.MARCH, 15), @@ -157,6 +157,16 @@ class AcademicCalendarBusinessObjFormatCheckerTests { LocalDate.of(2021, Month.JULY, 31), LocalDate.of(2021, Month.JULY, 31) ), + Event( + "Período de ausência de atividade letiva (férias)", + LocalDate.of(2021, Month.AUGUST, 1), + LocalDate.of(2021, Month.AUGUST, 31) + ), + Event( + "Data limite para lançamento de classificações no Portal Académico (frequência, exames de época normal e de época de recurso)", + LocalDate.of(2021, Month.SEPTEMBER, 2), + LocalDate.of(2021, Month.SEPTEMBER, 2) + ), Event( "Data limite para lançamento de classificações no Portal Académico (época especial)", LocalDate.of(2021, Month.SEPTEMBER, 30), From 9db37cca30b455452fbdf96e119fce8f0b91486a Mon Sep 17 00:00:00 2001 From: Grimord Date: Thu, 1 Jul 2021 00:02:27 +0100 Subject: [PATCH 04/67] make startDate in IntegrationJobParameters nullable update job view to handle all dates in UTC time --- .../kotlin/org/ionproject/integration/application/JobEngine.kt | 2 +- .../infrastructure/repository/IntegrationJobRepository.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt b/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt index bb3ce9c8..f85f06ee 100644 --- a/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt +++ b/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt @@ -162,7 +162,7 @@ class JobEngine( data class IntegrationJobParameters( val creationDate: LocalDateTime, - val startDate: LocalDateTime, + val startDate: LocalDateTime?, val format: OutputFormat, val institution: InstitutionModel, val programme: ProgrammeModel? = null, diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/repository/IntegrationJobRepository.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/repository/IntegrationJobRepository.kt index af65ce62..038cbfa3 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/repository/IntegrationJobRepository.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/repository/IntegrationJobRepository.kt @@ -67,7 +67,7 @@ class IntegrationJobRepository( val parameters = JobEngine.IntegrationJobParameters( creationDate.toLocalDateTime(), - startDate.toLocalDateTime(), + startDate?.toLocalDateTime(), OutputFormat.of(format), institution, if (jobType != JobType.ACADEMIC_CALENDAR) From 1ca3a33290d301eb391284fe346805c282b67759 Mon Sep 17 00:00:00 2001 From: Grimord Date: Thu, 1 Jul 2021 00:02:37 +0100 Subject: [PATCH 05/67] make startDate in IntegrationJobParameters nullable update job view to handle all dates in UTC time --- .../integration/infrastructure/repository/Queries.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/repository/Queries.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/repository/Queries.kt index ea6a3ec6..8987d2c9 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/repository/Queries.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/repository/Queries.kt @@ -4,11 +4,11 @@ const val CREATE_JOBS_VIEW_QUERY = """create or replace view vw_job_detail as SELECT bje.job_instance_id as id , bji.job_name as name - , bje.create_time as creation_date - , bje.start_time as start_date + , bje.create_time at time zone 'utc' as creation_date + , bje.start_time at time zone 'utc' as start_date ,CASE WHEN bje.STATUS IN ('STARTED','STARTING') - AND bje.create_time < (CURRENT_TIMESTAMP - interval '1 hour') + AND (bje.create_time at time zone 'utc') < ((CURRENT_TIMESTAMP at time zone 'utc') - interval '1 hours') THEN 'FAILED' ELSE bje.status END as status From 0cd625ffd484b481fbac04c108e1b65f3cdfeccf Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Thu, 1 Jul 2021 23:12:37 +0100 Subject: [PATCH 06/67] Update IOnIntegration_Staging.yml --- .github/workflows/IOnIntegration_Staging.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/IOnIntegration_Staging.yml b/.github/workflows/IOnIntegration_Staging.yml index ec64ed51..4cf589dd 100644 --- a/.github/workflows/IOnIntegration_Staging.yml +++ b/.github/workflows/IOnIntegration_Staging.yml @@ -51,7 +51,7 @@ jobs: heroku_app_name: ${{secrets.HEROKU_APP_NAME}} heroku_email: ${{secrets.HEROKU_EMAIL}} usedocker: true - docker_heroku_process_type: worker + docker_heroku_process_type: web - run: | echo "::add-mask::$HD_SQL_HOST" echo "::add-mask::$HD_SQL_USER" @@ -72,4 +72,4 @@ jobs: if: ${{ env.ACT }} run: | echo GIT_SERVER_ADDRESS=$GIT_SERVER_ADDRESS - echo GIT_BRANCH=$GIT_BRANCH \ No newline at end of file + echo GIT_BRANCH=$GIT_BRANCH From dfac59b41a5e293a02e40154b44499a4277be2ff Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Fri, 2 Jul 2021 11:13:37 +0100 Subject: [PATCH 07/67] Minor improvement with stating that integration data is available publicly for consumption by anyone. New version of architecture diagram. --- README.md | 2 +- img/ion_integration_architecture.png | Bin 51528 -> 66900 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8dda6032..9b7c2294 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Academic information such as class schedules or academic calendars is often scat ![I-On Integration Architecture](img/ion_integration_architecture.png) -**I-On Integration** uses *batch processing* techniques to acquire and process all unstructured data and write it to the common **File Repository** (a Git repo whose sole purpose is to host this data). +**I-On Integration** uses *batch processing* techniques to acquire and process all unstructured data and write it to the common **File Repository** (a Git repo whose sole purpose is to host this data). The data is available for anyone that wishes to use their data on their own projects. It is available at [GitHub i-on Integration Data repository](https://github.com/i-on-project/integration-data). The **Scheduler** component, as the name suggests, is responsible for periodically triggering job executions through Integration's Web API (not yet available). diff --git a/img/ion_integration_architecture.png b/img/ion_integration_architecture.png index 94d97efb3bad08ac1e695d0c9847c9c23d0a50c8..8c6c186588cd13a479a7d7356a9e94154e30030d 100644 GIT binary patch literal 66900 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

|nt?(1XL`GtH&t7Qs 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 aGbuv)JEX`j?Q}TcM@2zHzD)LM!2bjOY^RC< literal 51528 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 E0J}%K9RL6T From a51956d1028fa3cbdfb3b3569781a5d510b398b0 Mon Sep 17 00:00:00 2001 From: Cristiano Morgado Date: Fri, 2 Jul 2021 21:18:13 +0100 Subject: [PATCH 08/67] Update production pipeline to deploy to GHCR (#280) --- .../workflows/IOnIntegration_Production.yml | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/.github/workflows/IOnIntegration_Production.yml b/.github/workflows/IOnIntegration_Production.yml index 79fd55ce..46794ed7 100644 --- a/.github/workflows/IOnIntegration_Production.yml +++ b/.github/workflows/IOnIntegration_Production.yml @@ -2,18 +2,47 @@ name: I-On Integration Production on: push: + branches: ['master'] tags: - v* +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} jobs: push_production: runs-on: ubuntu-latest if: github.event_name == 'push' && contains(github.ref, 'tags') + permissions: + contents: read + packages: write steps: - name: Checkout uses: actions/checkout@v2 - - name: Build image - id: production_build_image - run: ./gradlew buildDockerImage \ No newline at end of file + - name: Login to Container Registry + uses: docker/login-action@v1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v3 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + + - name: Build and push Docker image + uses: docker/build-push-action@v2 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file From 9c172f50d2dca1ad5f04b435dab7ce5b9f1f1a34 Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Sat, 3 Jul 2021 15:09:00 +0100 Subject: [PATCH 09/67] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b7c2294..7610ce0d 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Academic information such as class schedules or academic calendars is often scat ![I-On Integration Architecture](img/ion_integration_architecture.png) -**I-On Integration** uses *batch processing* techniques to acquire and process all unstructured data and write it to the common **File Repository** (a Git repo whose sole purpose is to host this data). The data is available for anyone that wishes to use their data on their own projects. It is available at [GitHub i-on Integration Data repository](https://github.com/i-on-project/integration-data). +**I-On Integration** uses *batch processing* techniques to acquire and process all unstructured data and write it to the common **File Repository** (a Git repo whose sole purpose is to host this data). The data is available for anyone that wishes to use this data on their own projects. It is available at [GitHub i-on Integration Data repository](https://github.com/i-on-project/integration-data). The **Scheduler** component, as the name suggests, is responsible for periodically triggering job executions through Integration's Web API (not yet available). From fff797fc73d3ccd2600b4bc03ff499cdd178b649 Mon Sep 17 00:00:00 2001 From: Grimord Date: Sat, 3 Jul 2021 20:21:14 +0100 Subject: [PATCH 10/67] add InputProcessorTests small refactor InputProcessor.kt to ease unit testing small refactor to OutputFormat.kt to ease unit testing --- .../infrastructure/file/OutputFormat.kt | 4 +- .../ui/controller/JobController.kt | 2 - .../integration/ui/dto/InputProcessor.kt | 16 +- .../integration/ui/dto/InputProcessorTests.kt | 250 ++++++++++++++++++ 4 files changed, 264 insertions(+), 8 deletions(-) create mode 100644 src/test/kotlin/org/ionproject/integration/ui/dto/InputProcessorTests.kt diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/file/OutputFormat.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/file/OutputFormat.kt index a58e364f..5c204d2b 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/file/OutputFormat.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/file/OutputFormat.kt @@ -2,6 +2,8 @@ package org.ionproject.integration.infrastructure.file import org.ionproject.integration.infrastructure.exception.ArgumentException +internal const val INVALID_FORMAT_ERROR = "Invalid format: %s" + enum class OutputFormat(val extension: String) { YAML(".yml"), JSON(".json"); @@ -9,6 +11,6 @@ enum class OutputFormat(val extension: String) { companion object { fun of(name: String): OutputFormat = values().firstOrNull { it.name.equals(name.trim(), ignoreCase = true) } - ?: throw ArgumentException("Invalid format: $name") + ?: throw ArgumentException(INVALID_FORMAT_ERROR.format(name.trim())) } } diff --git a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt index 9b5dccfa..48e92d59 100644 --- a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt +++ b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt @@ -4,7 +4,6 @@ import org.ionproject.integration.application.JobEngine import org.ionproject.integration.ui.dto.CreateJobDto import org.ionproject.integration.ui.dto.InputProcessor import org.slf4j.LoggerFactory -import org.springframework.batch.core.explore.JobExplorer import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping @@ -17,7 +16,6 @@ import org.springframework.web.bind.annotation.RestController class JobController( val jobEngine: JobEngine, val inputProcessor: InputProcessor, - val jobExplorer: JobExplorer ) { private val logger = LoggerFactory.getLogger(JobController::class.java) diff --git a/src/main/kotlin/org/ionproject/integration/ui/dto/InputProcessor.kt b/src/main/kotlin/org/ionproject/integration/ui/dto/InputProcessor.kt index 8c555d0e..6d2c5b02 100644 --- a/src/main/kotlin/org/ionproject/integration/ui/dto/InputProcessor.kt +++ b/src/main/kotlin/org/ionproject/integration/ui/dto/InputProcessor.kt @@ -7,6 +7,12 @@ import org.ionproject.integration.infrastructure.repository.IInstitutionReposito import org.ionproject.integration.infrastructure.repository.IProgrammeRepository import org.springframework.stereotype.Component +internal const val INVALID_JOB_TYPE_ERROR = "Invalid Job Type: %s" +internal const val MISSING_PARAMETER_ERROR = "Parameter '%s' missing" +internal const val FORMAT = "format" +internal const val INSTITUTION = "institution" +internal const val PROGRAMME = "programme" + @Component class InputProcessor( val institutionRepo: IInstitutionRepository, @@ -18,7 +24,7 @@ class InputProcessor( } private fun getValidatedDto(dto: CreateJobDto): SafeJobDto { - val type = JobType.of(dto.type) ?: throw ArgumentException("Invalid Job Type: ${dto.type}") + val type = JobType.of(dto.type) ?: throw ArgumentException(INVALID_JOB_TYPE_ERROR.format(dto.type?.trim())) validateDto(dto, type) return dto.toSafeDto(type) @@ -26,11 +32,11 @@ class InputProcessor( private fun validateDto(dto: CreateJobDto, jobType: JobType) { runCatching { - requireNotNull(dto.format) { "format" } - requireNotNull(dto.institution) { "institution" } + require(dto.format?.isNotBlank() ?: false) { FORMAT } + require(dto.institution?.isNotBlank() ?: false) { INSTITUTION } if (jobType == JobType.TIMETABLE || jobType == JobType.EXAM_SCHEDULE) - requireNotNull(dto.programme) { "programme" } - }.onFailure { throw ArgumentException("Parameter '${it.message}' missing") } + require(dto.programme?.isNotBlank() ?: false) { PROGRAMME } + }.onFailure { throw ArgumentException(MISSING_PARAMETER_ERROR.format(it.message)) } } } diff --git a/src/test/kotlin/org/ionproject/integration/ui/dto/InputProcessorTests.kt b/src/test/kotlin/org/ionproject/integration/ui/dto/InputProcessorTests.kt new file mode 100644 index 00000000..53591801 --- /dev/null +++ b/src/test/kotlin/org/ionproject/integration/ui/dto/InputProcessorTests.kt @@ -0,0 +1,250 @@ +package org.ionproject.integration.ui.dto + +import org.ionproject.integration.application.JobEngine +import org.ionproject.integration.application.job.JobType +import org.ionproject.integration.domain.common.InstitutionModel +import org.ionproject.integration.domain.common.ProgrammeModel +import org.ionproject.integration.infrastructure.exception.ArgumentException +import org.ionproject.integration.infrastructure.file.INVALID_FORMAT_ERROR +import org.ionproject.integration.infrastructure.file.OutputFormat +import org.ionproject.integration.infrastructure.repository.IInstitutionRepository +import org.ionproject.integration.infrastructure.repository.IProgrammeRepository +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import java.net.URI + +private const val TEST_SCHOOL_NAME = "Test School" +private const val TEST_SCHOOL_ACRONYM = "TS" +private const val TEST_PROGRAMME_ACRONYM = "SP" +private const val TEST_PROGRAMME_NAME = "Some Engineering Programme I Guess" +private const val TEST_SCHOOL_ID = "test.school" +private val TEST_URI = URI("test.school./someFile.pdf") + +class InputProcessorTests { + private val testInstitution = InstitutionModel( + TEST_SCHOOL_NAME, + TEST_SCHOOL_ACRONYM, + TEST_SCHOOL_ID, + TEST_URI + ) + + private val testProgramme = ProgrammeModel( + institutionModel = testInstitution, + name = TEST_PROGRAMME_NAME, + acronym = TEST_PROGRAMME_ACRONYM, + timetableUri = TEST_URI + ) + + private val mockInstitutionRepoOK = mock { + on { getInstitutionByIdentifier(any()) } doReturn testInstitution + } + private val mockProgrammeRepoOK = mock { + on { getProgrammeByAcronymAndInstitution(any(), any()) } doReturn testProgramme + } + + private val inputProcessor = InputProcessor(mockInstitutionRepoOK, mockProgrammeRepoOK) + + @Test + fun `when given a valid CALENDAR job then return job request OK`() { + val createCalendarJob = CreateJobDto( + institution = TEST_SCHOOL_ID, + format = OutputFormat.JSON.name, + type = JobType.ACADEMIC_CALENDAR.identifier + ) + + val expected = JobEngine.CalendarJobRequest( + format = OutputFormat.JSON, + institution = testInstitution, + ) + val actual = inputProcessor.getJobRequest(createCalendarJob) + + assertEquals(expected, actual) + } + + @Test + fun `when given a valid job with extra whitespace then trim and return job request OK`() { + val createCalendarJob = CreateJobDto( + institution = " $TEST_SCHOOL_ID ", + format = " ${OutputFormat.JSON.name} ", + type = " ${JobType.ACADEMIC_CALENDAR.identifier} " + ) + + val expected = JobEngine.CalendarJobRequest( + format = OutputFormat.JSON, + institution = testInstitution, + ) + val actual = inputProcessor.getJobRequest(createCalendarJob) + + assertEquals(expected, actual) + } + + @Test + fun `when given a valid TIMETABLE job then return job request OK`() { + val createTimetableJob = CreateJobDto( + institution = TEST_SCHOOL_ID, + programme = TEST_PROGRAMME_ACRONYM, + format = OutputFormat.YAML.name, + type = JobType.TIMETABLE.identifier + ) + + val expected = JobEngine.TimetableJobRequest( + format = OutputFormat.YAML, + institution = testInstitution, + programme = testProgramme + ) + val actual = inputProcessor.getJobRequest(createTimetableJob) + + assertEquals(expected, actual) + } + + @Test + fun `when given an empty job then throw exception`() { + val expected = INVALID_JOB_TYPE_ERROR.format("null") + + val ex = assertThrows { + inputProcessor.getJobRequest(CreateJobDto()) + } + + assertEquals(expected, ex.message) + } + + @Test + fun `when given job with empty format then throw exception`() { + val expected = MISSING_PARAMETER_ERROR.format(FORMAT) + + val ex = assertThrows { + val jobDto = CreateJobDto( + institution = "ok", + format = " ", + type = JobType.ACADEMIC_CALENDAR.identifier + ) + inputProcessor.getJobRequest(jobDto) + } + + assertEquals(expected, ex.message) + } + + @Test + fun `when given job with invalid format then throw exception`() { + val expected = INVALID_FORMAT_ERROR.format("jason") + + val ex = assertThrows { + val jobDto = CreateJobDto( + institution = "ok", + format = " jason", + type = JobType.ACADEMIC_CALENDAR.identifier + ) + inputProcessor.getJobRequest(jobDto) + } + + assertEquals(expected, ex.message) + } + + @Test + fun `when given job with null format then throw exception`() { + val expected = MISSING_PARAMETER_ERROR.format(FORMAT) + + val ex = assertThrows { + val jobDto = CreateJobDto( + institution = "ok", + type = JobType.ACADEMIC_CALENDAR.identifier + ) + inputProcessor.getJobRequest(jobDto) + } + + assertEquals(expected, ex.message) + } + + @Test + fun `when given job with empty institution then throw exception`() { + val expected = MISSING_PARAMETER_ERROR.format(INSTITUTION) + + val ex = assertThrows { + val jobDto = CreateJobDto( + institution = " ", + format = OutputFormat.YAML.name, + type = JobType.ACADEMIC_CALENDAR.identifier + ) + inputProcessor.getJobRequest(jobDto) + } + + assertEquals(expected, ex.message) + } + + @Test + fun `when given job with null institution then throw exception`() { + val expected = MISSING_PARAMETER_ERROR.format(INSTITUTION) + + val ex = assertThrows { + val jobDto = CreateJobDto( + format = OutputFormat.YAML.name, + type = JobType.ACADEMIC_CALENDAR.identifier + ) + inputProcessor.getJobRequest(jobDto) + } + + assertEquals(expected, ex.message) + } + + @Test + fun `when given job with empty programme then throw exception`() { + val expected = MISSING_PARAMETER_ERROR.format(PROGRAMME) + + val ex = assertThrows { + val jobDto = CreateJobDto( + institution = TEST_SCHOOL_ID, + programme = " ", + format = OutputFormat.YAML.name, + type = JobType.TIMETABLE.identifier + ) + inputProcessor.getJobRequest(jobDto) + } + + assertEquals(expected, ex.message) + } + + @Test + fun `when given job with null programme then throw exception`() { + val expected = MISSING_PARAMETER_ERROR.format(PROGRAMME) + + val ex = assertThrows { + val jobDto = CreateJobDto( + institution = TEST_SCHOOL_ID, + format = OutputFormat.YAML.name, + type = JobType.TIMETABLE.identifier + ) + inputProcessor.getJobRequest(jobDto) + } + + assertEquals(expected, ex.message) + } + + @Test + fun `when given an empty job type then throw exception`() { + val expected = INVALID_JOB_TYPE_ERROR.format("null") + + val ex = assertThrows { + inputProcessor.getJobRequest( + CreateJobDto(" ") + ) + } + + assertEquals(expected, ex.message) + } + + @Test + fun `when given an invalid job type then throw exception`() { + val jobType = " invalid_job " + val expected = INVALID_JOB_TYPE_ERROR.format(jobType.trim()) + + val ex = assertThrows { + inputProcessor.getJobRequest(CreateJobDto(type = jobType)) + } + + assertEquals(expected, ex.message) + } +} From e736d85f6dd8bbd8795930be3a348aaeb0a9afcb Mon Sep 17 00:00:00 2001 From: Grimord Date: Sat, 3 Jul 2021 20:40:55 +0100 Subject: [PATCH 11/67] add OutputFormatTests add support for alternative format name for YAML (as yml) in OutputFormat.kt --- .../infrastructure/file/OutputFormat.kt | 17 ++- .../infrastructure/file/OutputFormatTests.kt | 116 ++++++++++++++++++ 2 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 src/test/kotlin/org/ionproject/integration/infrastructure/file/OutputFormatTests.kt diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/file/OutputFormat.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/file/OutputFormat.kt index 5c204d2b..916b079c 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/file/OutputFormat.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/file/OutputFormat.kt @@ -1,16 +1,23 @@ package org.ionproject.integration.infrastructure.file import org.ionproject.integration.infrastructure.exception.ArgumentException +import org.ionproject.integration.infrastructure.text.containsCaseInsensitive internal const val INVALID_FORMAT_ERROR = "Invalid format: %s" -enum class OutputFormat(val extension: String) { - YAML(".yml"), +enum class OutputFormat(val extension: String, private val alternativeNames: List = emptyList()) { + YAML(".yml", listOf("yml")), JSON(".json"); companion object { - fun of(name: String): OutputFormat = - values().firstOrNull { it.name.equals(name.trim(), ignoreCase = true) } - ?: throw ArgumentException(INVALID_FORMAT_ERROR.format(name.trim())) + fun of(name: String): OutputFormat { + val trimName = name.trim() + return values().firstOrNull { format -> + format.nameEquals(trimName) || format.alternativeNames.containsCaseInsensitive(trimName) + } + ?: throw ArgumentException(INVALID_FORMAT_ERROR.format(trimName)) + } } + + private fun nameEquals(name: String): Boolean = this.name.equals(name, ignoreCase = true) } diff --git a/src/test/kotlin/org/ionproject/integration/infrastructure/file/OutputFormatTests.kt b/src/test/kotlin/org/ionproject/integration/infrastructure/file/OutputFormatTests.kt new file mode 100644 index 00000000..5d414a75 --- /dev/null +++ b/src/test/kotlin/org/ionproject/integration/infrastructure/file/OutputFormatTests.kt @@ -0,0 +1,116 @@ +package org.ionproject.integration.infrastructure.file + +import org.ionproject.integration.infrastructure.exception.ArgumentException +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class OutputFormatTests { + @Test + fun `when given JSON format then assert extension is correct`() { + val expected = ".json" + val actual = OutputFormat.JSON.extension + + assertEquals(expected, actual) + } + + @Test + fun `when given YAML format then assert extension is correct`() { + val expected = ".yml" + val actual = OutputFormat.YAML.extension + + assertEquals(expected, actual) + } + + @Test + fun `when given json in factory method then return JSON format`() { + val expected = OutputFormat.JSON + val actual = OutputFormat.of("json") + + assertEquals(expected, actual) + } + + @Test + fun `when given json in uppercase in factory method then return JSON format`() { + val expected = OutputFormat.JSON + val actual = OutputFormat.of("JSON") + + assertEquals(expected, actual) + } + + @Test + fun `when given json with leading or trailing space in factory method then return JSON format`() { + val expected = OutputFormat.JSON + val actual = OutputFormat.of(" JsON ") + + assertEquals(expected, actual) + } + + @Test + fun `when given yaml in factory method then return JSON format`() { + val expected = OutputFormat.YAML + val actual = OutputFormat.of("yaml") + + assertEquals(expected, actual) + } + + @Test + fun `when given yml in factory method then return JSON format`() { + val expected = OutputFormat.YAML + val actual = OutputFormat.of("yml") + + assertEquals(expected, actual) + } + + @Test + fun `when given YAML in uppercase in factory method then return JSON format`() { + val expected = OutputFormat.YAML + val actual = OutputFormat.of("YAML") + + assertEquals(expected, actual) + } + + @Test + fun `when given YML in uppercase in factory method then return JSON format`() { + val expected = OutputFormat.YAML + val actual = OutputFormat.of("YML") + + assertEquals(expected, actual) + } + + @Test + fun `when given YAML with leading or trailing space in factory method then return JSON format`() { + val expected = OutputFormat.YAML + val actual = OutputFormat.of(" YaMl ") + + assertEquals(expected, actual) + } + + @Test + fun `when given YML with leading or trailing space in factory method then return JSON format`() { + val expected = OutputFormat.YAML + val actual = OutputFormat.of(" YMl ") + + assertEquals(expected, actual) + } + + @Test + fun `when given empty format then error`() { + val expectedErrorMessage = INVALID_FORMAT_ERROR.format("") + val ex = assertThrows { + OutputFormat.of("") + } + + assertEquals(expectedErrorMessage, ex.message) + } + + @Test + fun `when given unsupported format then error`() { + val expectedErrorMessage = INVALID_FORMAT_ERROR.format("csv") + val ex = assertThrows { + OutputFormat.of("csv") + } + + assertEquals(expectedErrorMessage, ex.message) + } +} From 3180d9f15a52a5a4c9139776e4ee2cc20dea8d47 Mon Sep 17 00:00:00 2001 From: Grimord Date: Sun, 4 Jul 2021 00:35:53 +0100 Subject: [PATCH 12/67] add JobController tests improve job controller response status add job controller Location header on job creation --- .../ui/controller/JobController.kt | 37 ++++-- .../ui/controller/JobControllerTests.kt | 111 ++++++++++++++++++ 2 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 src/test/kotlin/org/ionproject/integration/ui/controller/JobControllerTests.kt diff --git a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt index 48e92d59..e86a1b33 100644 --- a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt +++ b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt @@ -10,9 +10,13 @@ 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 javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +internal const val JOBS_URI = "/jobs" @RestController -@RequestMapping("/jobs") +@RequestMapping(JOBS_URI) class JobController( val jobEngine: JobEngine, val inputProcessor: InputProcessor, @@ -21,25 +25,34 @@ class JobController( private val logger = LoggerFactory.getLogger(JobController::class.java) @PostMapping(consumes = ["application/json"]) - fun createTimetableJob(@RequestBody body: CreateJobDto): String { + fun createTimetableJob( + @RequestBody body: CreateJobDto, + servletRequest: HttpServletRequest, + response: HttpServletResponse + ): String { val request = inputProcessor.getJobRequest(body) val requestResult = jobEngine.runJob(request) return when (requestResult.result) { - JobEngine.JobExecutionResult.CREATED -> "Created ${request.javaClass.simpleName} job with ID ${requestResult.jobId}" - else -> "FAILED: ${requestResult.result}" + JobEngine.JobExecutionResult.CREATED -> { + response.status = HttpServletResponse.SC_CREATED + response.addHeader("Location", servletRequest.getLocationForJobRequest(requestResult)) + "Created ${request.javaClass.simpleName} job with ID ${requestResult.jobId}" + } + else -> { + logger.error("Job creation failed: $body") + response.status = HttpServletResponse.SC_BAD_REQUEST + "FAILED: ${requestResult.result}" + } } } @GetMapping - fun getJobs(): List { - val jobs = jobEngine.getRunningJobs() - return jobs - } + fun getJobs(): List = jobEngine.getRunningJobs() @GetMapping("/{id}") - fun getJobDetails(@PathVariable id: Long): JobEngine.IntegrationJob { - val job = jobEngine.getJob(id) - return job - } + fun getJobDetails(@PathVariable id: Long): JobEngine.IntegrationJob = jobEngine.getJob(id) + + private fun HttpServletRequest.getLocationForJobRequest(jobStatus: JobEngine.JobStatus): String = + "$localName:${localPort}$JOBS_URI/${jobStatus.jobId}" } diff --git a/src/test/kotlin/org/ionproject/integration/ui/controller/JobControllerTests.kt b/src/test/kotlin/org/ionproject/integration/ui/controller/JobControllerTests.kt new file mode 100644 index 00000000..00881a84 --- /dev/null +++ b/src/test/kotlin/org/ionproject/integration/ui/controller/JobControllerTests.kt @@ -0,0 +1,111 @@ +package org.ionproject.integration.ui.controller + +import org.hamcrest.CoreMatchers.containsString +import org.ionproject.integration.application.JobEngine +import org.ionproject.integration.application.job.JobType +import org.ionproject.integration.domain.common.InstitutionModel +import org.ionproject.integration.infrastructure.exception.ArgumentException +import org.ionproject.integration.infrastructure.file.OutputFormat +import org.ionproject.integration.ui.dto.InputProcessor +import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.whenever +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import java.net.URI +import java.time.LocalDateTime + +@WebMvcTest +class JobControllerTests { + @MockBean + private lateinit var jobEngine: JobEngine + + @MockBean + private lateinit var inputProcessor: InputProcessor + + @Autowired + private lateinit var mockMvc: MockMvc + + private val mockInstitution = InstitutionModel("test", "test", "test", URI("www.test.com")) + + @Test + fun `when receiving a request to list running jobs then return OK`() { + val date = LocalDateTime.of(2020, 6, 30, 15, 3) + + val jobParams = JobEngine.IntegrationJobParameters( + creationDate = date, + startDate = date, + format = OutputFormat.YAML, + institution = mockInstitution, + uri = URI("www.test.com") + ) + + val mockJob1 = JobEngine.IntegrationJob( + type = JobType.TIMETABLE, + status = JobEngine.JobStatus(1, JobEngine.JobExecutionResult.RUNNING), + parameters = jobParams + ) + + val mockJob2 = JobEngine.IntegrationJob( + type = JobType.ACADEMIC_CALENDAR, + status = JobEngine.JobStatus(3, JobEngine.JobExecutionResult.CREATED), + parameters = jobParams + ) + + val expectedResponse = + """[{"type":"TIMETABLE","status":{"jobId":1,"result":"RUNNING"},"parameters":{"creationDate":"2020-06-30T15:03:00","startDate":"2020-06-30T15:03:00","format":"YAML","institution":{"name":"test","acronym":"test","identifier":"test","academicCalendarUri":"www.test.com"},"programme":null,"uri":"www.test.com"}},{"type":"ACADEMIC_CALENDAR","status":{"jobId":3,"result":"CREATED"},"parameters":{"creationDate":"2020-06-30T15:03:00","startDate":"2020-06-30T15:03:00","format":"YAML","institution":{"name":"test","acronym":"test","identifier":"test","academicCalendarUri":"www.test.com"},"programme":null,"uri":"www.test.com"}}]""" + + whenever(jobEngine.getRunningJobs()) doReturn listOf(mockJob1, mockJob2) + + mockMvc.perform(get(JOBS_URI)) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().string(containsString(expectedResponse))) + } + + @Test + fun `when receiving a run job request then return CREATED`() { + + whenever(inputProcessor.getJobRequest(any())) doReturn JobEngine.CalendarJobRequest( + OutputFormat.YAML, + mockInstitution + ) + whenever(jobEngine.runJob(any())) doReturn JobEngine.JobStatus(1, JobEngine.JobExecutionResult.CREATED) + + val expectedResponse = "Created CalendarJobRequest job with ID 1" + + mockMvc.perform( + post(JOBS_URI) + .contentType(MediaType.APPLICATION_JSON) + .content("{}") + ) + .andExpect(status().isCreated) + .andExpect(header().string("Location", """localhost:80/jobs/1""")) + .andExpect(content().string(containsString(expectedResponse))) + } + + @Test + fun `when receiving a bad job request then return BAD_REQUEST`() { + whenever(inputProcessor.getJobRequest(any())) doThrow ArgumentException("You've been a bad, bad boy!") + + val expectedResponse = "You've been a bad, bad boy!" + + mockMvc.perform( + post(JOBS_URI) + .contentType(MediaType.APPLICATION_JSON) + .content("{}") + ) + .andExpect(status().isBadRequest) + .andExpect(content().string(containsString(expectedResponse))) + } +} From 6c8e89dc3dbedcbd7c70c23675cebfb995d1f8a7 Mon Sep 17 00:00:00 2001 From: Grimord Date: Sun, 4 Jul 2021 18:33:45 +0100 Subject: [PATCH 13/67] remove git-server binary data used to setup local env as it is no longer needed update docker-compose.yml to create repository programmatically on start up --- docker-compose.yml | 16 ++++++++++++---- git-server/data.mv.db | Bin 241664 -> 0 bytes git-server/database.conf | 10 ---------- .../root/integration-data.git/HEAD | 1 - .../root/integration-data.git/config | 7 ------- .../0b/3deb0e860b57f2b580eecd18ee484fe113aadf | Bin 53 -> 0 bytes .../59/06ae9e97313fb554f5836167f8738af35a9a07 | Bin 118 -> 0 bytes .../8d/2e77da69377b86eb13232829ed054d601c9f38 | Bin 53 -> 0 bytes .../refs/heads/experimental | 1 - .../integration-data.git/refs/heads/master | 1 - .../root/integration-data.wiki.git/HEAD | 1 - .../root/integration-data.wiki.git/config | 7 ------- .../02/ade229702f567984e9695cc6e30aed9d35d236 | Bin 51 -> 0 bytes .../78/f09183f4fcdb126c2c680af728650e1f12705e | Bin 54 -> 0 bytes .../8f/646090cb47397c2e4ff528b6df203950ce0429 | Bin 119 -> 0 bytes .../refs/heads/master | 1 - 16 files changed, 12 insertions(+), 33 deletions(-) delete mode 100644 git-server/data.mv.db delete mode 100644 git-server/database.conf delete mode 100644 git-server/repositories/root/integration-data.git/HEAD delete mode 100644 git-server/repositories/root/integration-data.git/config delete mode 100644 git-server/repositories/root/integration-data.git/objects/0b/3deb0e860b57f2b580eecd18ee484fe113aadf delete mode 100644 git-server/repositories/root/integration-data.git/objects/59/06ae9e97313fb554f5836167f8738af35a9a07 delete mode 100644 git-server/repositories/root/integration-data.git/objects/8d/2e77da69377b86eb13232829ed054d601c9f38 delete mode 100644 git-server/repositories/root/integration-data.git/refs/heads/experimental delete mode 100644 git-server/repositories/root/integration-data.git/refs/heads/master delete mode 100644 git-server/repositories/root/integration-data.wiki.git/HEAD delete mode 100644 git-server/repositories/root/integration-data.wiki.git/config delete mode 100644 git-server/repositories/root/integration-data.wiki.git/objects/02/ade229702f567984e9695cc6e30aed9d35d236 delete mode 100644 git-server/repositories/root/integration-data.wiki.git/objects/78/f09183f4fcdb126c2c680af728650e1f12705e delete mode 100644 git-server/repositories/root/integration-data.wiki.git/objects/8f/646090cb47397c2e4ff528b6df203950ce0429 delete mode 100644 git-server/repositories/root/integration-data.wiki.git/refs/heads/master diff --git a/docker-compose.yml b/docker-compose.yml index 8deda1a9..2c0a2117 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,8 +23,8 @@ services: depends_on: ion-db: condition: service_started - git-server: - condition: service_healthy + git-setup: + condition: service_started ion-db: image: postgres:13.2-alpine ports: @@ -38,11 +38,19 @@ services: ports: - "${GIT_PORT}:8080" volumes: - - "./git-server:/gitbucket" + - git-data:/gitbucket healthcheck: test: [ "CMD", "curl", "-f", "http://localhost:${GIT_PORT}" ] interval: 30s timeout: 10s retries: 5 + git-setup: + image: gitbucket/gitbucket:4.35.3 + depends_on: + git-server: + condition: service_healthy + restart: "no" + entrypoint: [ "curl", "-X", "POST", "-u", "root:root", "git-server:${GIT_PORT}/api/v3/user/repos", "-d", "{\"name\":\"integration-data\", \"auto_init\": true}" ] volumes: - db-data: \ No newline at end of file + db-data: + git-data: \ No newline at end of file diff --git a/git-server/data.mv.db b/git-server/data.mv.db deleted file mode 100644 index 3b0d531a690fc3e794795e8c9ae29adc5f51dc0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 241664 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 L%zO&~D6|deGVd0) diff --git a/git-server/repositories/root/integration-data.git/objects/59/06ae9e97313fb554f5836167f8738af35a9a07 b/git-server/repositories/root/integration-data.git/objects/59/06ae9e97313fb554f5836167f8738af35a9a07 deleted file mode 100644 index bd94450c264c555bcff6593b22b5325a47f27ba5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118 zcmV-+0Ez#2oQ;h;4#F@DK$&w2?<{tkgdicr+GFJ7sF6}ErRM%Xxd3l?+tbUkZB2ok zKj~lwOmpFc96Tm>sZ(+RnRB7QY=f;#G{;z~i7#Dczz|Wmef{W?V_#+0JCL8yQLtBn YGk!4n*eL%UnC02D#SXvn4H!W)=0w>#YXATM diff --git a/git-server/repositories/root/integration-data.git/objects/8d/2e77da69377b86eb13232829ed054d601c9f38 b/git-server/repositories/root/integration-data.git/objects/8d/2e77da69377b86eb13232829ed054d601c9f38 deleted file mode 100644 index b0d4a78278240ee051ec792dcb03326f5b7bc6b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53 zcmV-50LuS(oJ-2dPf{>7V93lXNlh9WiT`_Ff%bx@W{_i)yqv`sQ56k`OBZ%LOD7aT;DZP`Q(KP J;s7;w4+Ja+78d{j diff --git a/git-server/repositories/root/integration-data.wiki.git/objects/78/f09183f4fcdb126c2c680af728650e1f12705e b/git-server/repositories/root/integration-data.wiki.git/objects/78/f09183f4fcdb126c2c680af728650e1f12705e deleted file mode 100644 index 24d4dbfe54123e29d97545da8132d33694bf6fed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54 zcmV-60LlM&oJ-2dPf{?pU!@&kHEGLySuJzf9+ diff --git a/git-server/repositories/root/integration-data.wiki.git/refs/heads/master b/git-server/repositories/root/integration-data.wiki.git/refs/heads/master deleted file mode 100644 index 9eeee13c..00000000 --- a/git-server/repositories/root/integration-data.wiki.git/refs/heads/master +++ /dev/null @@ -1 +0,0 @@ -8f646090cb47397c2e4ff528b6df203950ce0429 From b88dbbb5e9655d6582a66a03cb4cb82a01b4bf11 Mon Sep 17 00:00:00 2001 From: Grimord Date: Sun, 4 Jul 2021 18:42:34 +0100 Subject: [PATCH 14/67] update README.md to match changes remove Windows 10 warning that no longer appears due to the removal of the bind mount --- README.md | 6 ++---- img/bindmountwarning.jpg | Bin 18813 -> 0 bytes 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 img/bindmountwarning.jpg diff --git a/README.md b/README.md index 7610ce0d..98cc345c 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,6 @@ By **default** the app containers will be accessible via the following ports: If you wish to change these values please refer to the [Customizing Containers](#customizing-containers) section. -On Windows 10 you may see a **warning** about poor performance due to filesharing to a WSL container. This is due to the fact that we make use of [Docker Bind Mounts](https://docs.docker.com/storage/bind-mounts/) to share the necessary server files. - -![Warning](img/bindmountwarning.jpg) - #### Using the containerized database While the Integration app will be able to connect to the database out of the box without any human intervention you might also be interested in connecting directly to run your own queries. @@ -89,6 +85,8 @@ The Git server currently deployed for local development is based on the [GitBuck It's Web front-end can be accessed by pointing your browser to `localhost:8080` (default port, see the [Customizing Containers section](#customizing-containers) if you need to use a different port value). +On startup a repository named **"integration-data"** is created and initialized to mirror the existing production repository. + ##### Git Credentials The server is deployed with a single **account** that can be accessed by entering `root` for both username and password fields. diff --git a/img/bindmountwarning.jpg b/img/bindmountwarning.jpg deleted file mode 100644 index 211f8496aa0d32de589d1ef26349643f2ccf66ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18813 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 J+Ws4Ze*jY9jw%2E From 231d6d3f2bc9aba257be7114c81bbabbe2216985 Mon Sep 17 00:00:00 2001 From: Grimord Date: Sun, 4 Jul 2021 14:25:51 +0100 Subject: [PATCH 15/67] add context path to web API --- .../ui/controller/JobController.kt | 2 +- src/main/resources/application.properties | 5 +++- .../ui/controller/JobControllerTests.kt | 23 +++++++++++++++---- src/test/resources/application.properties | 2 ++ 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt index e86a1b33..df6764ce 100644 --- a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt +++ b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt @@ -54,5 +54,5 @@ class JobController( fun getJobDetails(@PathVariable id: Long): JobEngine.IntegrationJob = jobEngine.getJob(id) private fun HttpServletRequest.getLocationForJobRequest(jobStatus: JobEngine.JobStatus): String = - "$localName:${localPort}$JOBS_URI/${jobStatus.jobId}" + "$scheme://$localName:${localPort}$contextPath$JOBS_URI/${jobStatus.jobId}" } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d49c2228..9f3355b1 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,6 +1,10 @@ #spring spring.main.banner-mode=console +#spring web +server.servlet.context-path=/integration +server.port=${SERVER_PORT} + #spring batch spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.url=${SQL_HOST} @@ -11,7 +15,6 @@ spring.datasource.initialization-mode=always spring.datasource.hikari.initialization-fail-timeout=50000 spring.batch.initialize-schema=always spring.batch.job.enabled=false -server.port=${SERVER_PORT} #i-on ion.config-file=${CONFIG_FILE} diff --git a/src/test/kotlin/org/ionproject/integration/ui/controller/JobControllerTests.kt b/src/test/kotlin/org/ionproject/integration/ui/controller/JobControllerTests.kt index 00881a84..431ffb6a 100644 --- a/src/test/kotlin/org/ionproject/integration/ui/controller/JobControllerTests.kt +++ b/src/test/kotlin/org/ionproject/integration/ui/controller/JobControllerTests.kt @@ -1,5 +1,6 @@ package org.ionproject.integration.ui.controller +import org.assertj.core.api.Assertions.assertThat import org.hamcrest.CoreMatchers.containsString import org.ionproject.integration.application.JobEngine import org.ionproject.integration.application.job.JobType @@ -7,15 +8,19 @@ import org.ionproject.integration.domain.common.InstitutionModel import org.ionproject.integration.infrastructure.exception.ArgumentException import org.ionproject.integration.infrastructure.file.OutputFormat import org.ionproject.integration.ui.dto.InputProcessor +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow import org.mockito.kotlin.whenever import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.http.MediaType +import org.springframework.mock.web.MockServletContext +import org.springframework.test.context.TestPropertySource import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post @@ -26,6 +31,7 @@ import java.net.URI import java.time.LocalDateTime @WebMvcTest +@TestPropertySource("classpath:application.properties") class JobControllerTests { @MockBean private lateinit var jobEngine: JobEngine @@ -36,8 +42,17 @@ class JobControllerTests { @Autowired private lateinit var mockMvc: MockMvc + @Value("\${server.servlet.context-path}") + private lateinit var contextPath: String + private val mockInstitution = InstitutionModel("test", "test", "test", URI("www.test.com")) + @BeforeEach + fun setUp() { + assertThat(contextPath).isNotBlank + (mockMvc.dispatcherServlet.servletContext as MockServletContext).contextPath = contextPath + } + @Test fun `when receiving a request to list running jobs then return OK`() { val date = LocalDateTime.of(2020, 6, 30, 15, 3) @@ -67,7 +82,7 @@ class JobControllerTests { whenever(jobEngine.getRunningJobs()) doReturn listOf(mockJob1, mockJob2) - mockMvc.perform(get(JOBS_URI)) + mockMvc.perform(get("$contextPath$JOBS_URI").contextPath(contextPath)) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().string(containsString(expectedResponse))) @@ -85,12 +100,12 @@ class JobControllerTests { val expectedResponse = "Created CalendarJobRequest job with ID 1" mockMvc.perform( - post(JOBS_URI) + post("$contextPath$JOBS_URI").contextPath(contextPath) .contentType(MediaType.APPLICATION_JSON) .content("{}") ) .andExpect(status().isCreated) - .andExpect(header().string("Location", """localhost:80/jobs/1""")) + .andExpect(header().string("Location", """http://localhost:80$contextPath$JOBS_URI/1""")) .andExpect(content().string(containsString(expectedResponse))) } @@ -101,7 +116,7 @@ class JobControllerTests { val expectedResponse = "You've been a bad, bad boy!" mockMvc.perform( - post(JOBS_URI) + post("$contextPath$JOBS_URI").contextPath(contextPath) .contentType(MediaType.APPLICATION_JSON) .content("{}") ) diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index f266cd95..d7933fc5 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -8,6 +8,8 @@ spring.datasource.password= spring.batch.job.enabled = false +#spring web +server.servlet.context-path=/integration spring.mail.host=localhost spring.mail.username=alert-mailbox@domain.com From 6b0794c5edd82cb6e09df83d7a7ea29e1e0d1c41 Mon Sep 17 00:00:00 2001 From: Grimord Date: Sun, 4 Jul 2021 19:33:58 +0100 Subject: [PATCH 16/67] add ArgumentException error definition add Problem class to represent problem+json output improve wrong institution ID message on InstitutionRepositoryImpl.kt force ArgumentException errors to return new Problem type --- docs/infrastructure/ArgumentException.md | 3 ++ .../repository/InstitutionRepositoryImpl.kt | 2 +- .../ui/controller/ControllerConfig.kt | 13 ++++++-- .../ionproject/integration/ui/dto/Problem.kt | 30 +++++++++++++++++++ 4 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 docs/infrastructure/ArgumentException.md create mode 100644 src/main/kotlin/org/ionproject/integration/ui/dto/Problem.kt diff --git a/docs/infrastructure/ArgumentException.md b/docs/infrastructure/ArgumentException.md new file mode 100644 index 00000000..38b69c09 --- /dev/null +++ b/docs/infrastructure/ArgumentException.md @@ -0,0 +1,3 @@ +This **error** will occur in Integration's Web API associated with the status code [400: Bad Request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400) and signals that either an expected argument is missing or incorrect. + +The response body will be in the `application/json+problem` mediatype and its `detail` field will include more context-specific information about the error cause. \ No newline at end of file diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/repository/InstitutionRepositoryImpl.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/repository/InstitutionRepositoryImpl.kt index b432c151..f30d3193 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/repository/InstitutionRepositoryImpl.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/repository/InstitutionRepositoryImpl.kt @@ -39,7 +39,7 @@ class InstitutionRepositoryImpl : IInstitutionRepository { internal fun findInstitution(identifier: String, institutions: List): InstitutionDto = institutions .firstOrNull { it.identifier.equals(identifier, ignoreCase = true) } - ?: throw ArgumentException("Institution with $identifier not found") + ?: throw ArgumentException("Institution with ID '$identifier' not found") } internal data class InstitutionDto( diff --git a/src/main/kotlin/org/ionproject/integration/ui/controller/ControllerConfig.kt b/src/main/kotlin/org/ionproject/integration/ui/controller/ControllerConfig.kt index 77036251..f1a6f6a9 100644 --- a/src/main/kotlin/org/ionproject/integration/ui/controller/ControllerConfig.kt +++ b/src/main/kotlin/org/ionproject/integration/ui/controller/ControllerConfig.kt @@ -2,22 +2,29 @@ package org.ionproject.integration.ui.controller import org.ionproject.integration.infrastructure.exception.ArgumentException import org.ionproject.integration.infrastructure.exception.JobNotFoundException +import org.ionproject.integration.ui.dto.Problem import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.ControllerAdvice import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.context.request.ServletWebRequest import org.springframework.web.context.request.WebRequest import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler @ControllerAdvice class ControllerConfig : ResponseEntityExceptionHandler() { - // TODO: Use json+problem (?) @ExceptionHandler(value = [ArgumentException::class]) - fun handle(exception: ArgumentException, request: WebRequest): ResponseEntity { + fun handle(exception: ArgumentException, request: WebRequest): ResponseEntity { logger.error("Error processing request $request: ${exception.message}") - return handleExceptionInternal(exception, exception.message, HttpHeaders(), HttpStatus.BAD_REQUEST, request) + // return handleExceptionInternal(exception, exception.message, HttpHeaders(), HttpStatus.BAD_REQUEST, request) + // ver campos da request + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .contentType(MediaType.APPLICATION_PROBLEM_JSON) + .body(Problem.of(exception, (request as ServletWebRequest).request)) } @ExceptionHandler(value = [JobNotFoundException::class]) diff --git a/src/main/kotlin/org/ionproject/integration/ui/dto/Problem.kt b/src/main/kotlin/org/ionproject/integration/ui/dto/Problem.kt new file mode 100644 index 00000000..4e973ff9 --- /dev/null +++ b/src/main/kotlin/org/ionproject/integration/ui/dto/Problem.kt @@ -0,0 +1,30 @@ +package org.ionproject.integration.ui.dto + +import com.fasterxml.jackson.annotation.JsonInclude +import org.ionproject.integration.infrastructure.exception.ArgumentException +import org.springframework.http.HttpStatus +import javax.servlet.http.HttpServletRequest + +private const val ARGUMENT_EX_URI = + "https://github.com/i-on-project/integration/blob/master/docs/infrastructure/ArgumentException.md" + +@JsonInclude(JsonInclude.Include.NON_NULL) +data class Problem( + val type: String, + val title: String, + val status: Int? = null, + val detail: String? = null, + val instance: String? = null +) { + companion object Factory { + fun of(argumentException: ArgumentException, request: HttpServletRequest): Problem { + return Problem( + title = HttpStatus.BAD_REQUEST.reasonPhrase, + status = HttpStatus.BAD_REQUEST.value(), + detail = argumentException.message, + type = ARGUMENT_EX_URI, + instance = request.requestURI + ) + } + } +} From 7a164e1c44af2cbf5e3f0bec3af44138f76d428c Mon Sep 17 00:00:00 2001 From: Grimord Date: Sun, 4 Jul 2021 19:43:19 +0100 Subject: [PATCH 17/67] add JobNotFoundException definition make JobNotFoundException errors to return new Problem type --- docs/infrastructure/JobNotFoundException.md | 3 +++ .../ui/controller/ControllerConfig.kt | 12 +++++++----- .../ionproject/integration/ui/dto/Problem.kt | 18 ++++++++++++++---- 3 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 docs/infrastructure/JobNotFoundException.md diff --git a/docs/infrastructure/JobNotFoundException.md b/docs/infrastructure/JobNotFoundException.md new file mode 100644 index 00000000..8c8fd553 --- /dev/null +++ b/docs/infrastructure/JobNotFoundException.md @@ -0,0 +1,3 @@ +This **error** will occur in Integration's Web API associated with the status code [404: Not Found](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404) and indicates that the request Job ID does not exist. + +The response body will be in the `application/json+problem` mediatype and its `detail` field will include the given Job ID. The `instance` field will contain the requested path. \ No newline at end of file diff --git a/src/main/kotlin/org/ionproject/integration/ui/controller/ControllerConfig.kt b/src/main/kotlin/org/ionproject/integration/ui/controller/ControllerConfig.kt index f1a6f6a9..e4194f7f 100644 --- a/src/main/kotlin/org/ionproject/integration/ui/controller/ControllerConfig.kt +++ b/src/main/kotlin/org/ionproject/integration/ui/controller/ControllerConfig.kt @@ -3,7 +3,6 @@ package org.ionproject.integration.ui.controller import org.ionproject.integration.infrastructure.exception.ArgumentException import org.ionproject.integration.infrastructure.exception.JobNotFoundException import org.ionproject.integration.ui.dto.Problem -import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity @@ -19,8 +18,7 @@ class ControllerConfig : ResponseEntityExceptionHandler() { @ExceptionHandler(value = [ArgumentException::class]) fun handle(exception: ArgumentException, request: WebRequest): ResponseEntity { logger.error("Error processing request $request: ${exception.message}") - // return handleExceptionInternal(exception, exception.message, HttpHeaders(), HttpStatus.BAD_REQUEST, request) - // ver campos da request + return ResponseEntity .status(HttpStatus.BAD_REQUEST) .contentType(MediaType.APPLICATION_PROBLEM_JSON) @@ -28,8 +26,12 @@ class ControllerConfig : ResponseEntityExceptionHandler() { } @ExceptionHandler(value = [JobNotFoundException::class]) - fun handleNotFound(exception: JobNotFoundException, request: WebRequest): ResponseEntity { + fun handleNotFound(exception: JobNotFoundException, request: WebRequest): ResponseEntity { logger.error("Error processing request $request: ${exception.message}") - return handleExceptionInternal(exception, exception.message, HttpHeaders(), HttpStatus.NOT_FOUND, request) + + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .contentType(MediaType.APPLICATION_PROBLEM_JSON) + .body(Problem.of(exception, (request as ServletWebRequest).request)) } } diff --git a/src/main/kotlin/org/ionproject/integration/ui/dto/Problem.kt b/src/main/kotlin/org/ionproject/integration/ui/dto/Problem.kt index 4e973ff9..b32f7fb5 100644 --- a/src/main/kotlin/org/ionproject/integration/ui/dto/Problem.kt +++ b/src/main/kotlin/org/ionproject/integration/ui/dto/Problem.kt @@ -2,11 +2,12 @@ package org.ionproject.integration.ui.dto import com.fasterxml.jackson.annotation.JsonInclude import org.ionproject.integration.infrastructure.exception.ArgumentException +import org.ionproject.integration.infrastructure.exception.JobNotFoundException import org.springframework.http.HttpStatus import javax.servlet.http.HttpServletRequest -private const val ARGUMENT_EX_URI = - "https://github.com/i-on-project/integration/blob/master/docs/infrastructure/ArgumentException.md" +private const val ARGUMENT_EX_URI = "https://github.com/i-on-project/integration/blob/master/docs/infrastructure/ArgumentException.md" +private const val JOB_NOT_FOUND_EX_URI = "https://github.com/i-on-project/integration/blob/master/docs/infrastructure/JobNotFoundException.md" @JsonInclude(JsonInclude.Include.NON_NULL) data class Problem( @@ -17,14 +18,23 @@ data class Problem( val instance: String? = null ) { companion object Factory { - fun of(argumentException: ArgumentException, request: HttpServletRequest): Problem { - return Problem( + fun of(argumentException: ArgumentException, request: HttpServletRequest): Problem = + Problem( title = HttpStatus.BAD_REQUEST.reasonPhrase, status = HttpStatus.BAD_REQUEST.value(), detail = argumentException.message, type = ARGUMENT_EX_URI, instance = request.requestURI ) + + fun of(jobNotFoundException: JobNotFoundException, request: HttpServletRequest): Problem { + return Problem( + title = HttpStatus.NOT_FOUND.reasonPhrase, + status = HttpStatus.NOT_FOUND.value(), + detail = jobNotFoundException.message, + type = JOB_NOT_FOUND_EX_URI, + instance = request.requestURI + ) } } } From 0b0c418b1d513998fc39be5089e0ec9111111f8e Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 02:00:32 +0100 Subject: [PATCH 18/67] add enddate field to job data --- .../integration/application/JobEngine.kt | 1 + .../repository/IntegrationJobRepository.kt | 21 ++++++++++--------- .../infrastructure/repository/Queries.kt | 19 +++++++++++++++++ 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt b/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt index f85f06ee..4a1214ca 100644 --- a/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt +++ b/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt @@ -163,6 +163,7 @@ class JobEngine( data class IntegrationJobParameters( val creationDate: LocalDateTime, val startDate: LocalDateTime?, + val endDate: LocalDateTime?, val format: OutputFormat, val institution: InstitutionModel, val programme: ProgrammeModel? = null, diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/repository/IntegrationJobRepository.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/repository/IntegrationJobRepository.kt index 038cbfa3..e7e44632 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/repository/IntegrationJobRepository.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/repository/IntegrationJobRepository.kt @@ -50,16 +50,16 @@ class IntegrationJobRepository( } private fun getJobDataFromResultSet(resultSet: ResultSet): JobEngine.IntegrationJob { - // TODO: create vars for indexes - val id = resultSet.getLong(1) - val jobIdentifier = resultSet.getString(2) - val creationDate = resultSet.getTimestamp(3) - val startDate = resultSet.getTimestamp(4) - val status = resultSet.getString(5) - val format = resultSet.getString(6) - val institutionIdentifier = resultSet.getString(7) - val programme = resultSet.getString(8) - val uri = resultSet.getString(9) + val id = resultSet.getLong(JobQueryFields.ID.index) + val jobIdentifier = resultSet.getString(JobQueryFields.NAME.index) + val creationDate = resultSet.getTimestamp(JobQueryFields.CREATION_DATE.index) + val startDate = resultSet.getTimestamp(JobQueryFields.START_DATE.index) + val endDate = resultSet.getTimestamp(JobQueryFields.END_DATE.index) + val status = resultSet.getString(JobQueryFields.STATUS.index) + val format = resultSet.getString(JobQueryFields.FORMAT.index) + val institutionIdentifier = resultSet.getString(JobQueryFields.INSTITUTION.index) + val programme = resultSet.getString(JobQueryFields.PROGRAMME.index) + val uri = resultSet.getString(JobQueryFields.URI.index) val jobType = JobType.of(jobIdentifier) ?: throw IllegalArgumentException("Invalid job: $jobIdentifier") @@ -68,6 +68,7 @@ class IntegrationJobRepository( val parameters = JobEngine.IntegrationJobParameters( creationDate.toLocalDateTime(), startDate?.toLocalDateTime(), + endDate?.toLocalDateTime(), OutputFormat.of(format), institution, if (jobType != JobType.ACADEMIC_CALENDAR) diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/repository/Queries.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/repository/Queries.kt index 8987d2c9..4b4a1002 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/repository/Queries.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/repository/Queries.kt @@ -1,11 +1,30 @@ package org.ionproject.integration.infrastructure.repository +/** + * Ordering on this enum MUST match ordering on CREATE_JOBS_VIEW_QUERY + */ +internal enum class JobQueryFields { + ID, + NAME, + CREATION_DATE, + START_DATE, + END_DATE, + STATUS, + FORMAT, + INSTITUTION, + PROGRAMME, + URI; + + val index = ordinal + 1 +} + const val CREATE_JOBS_VIEW_QUERY = """create or replace view vw_job_detail as SELECT bje.job_instance_id as id , bji.job_name as name , bje.create_time at time zone 'utc' as creation_date , bje.start_time at time zone 'utc' as start_date + , bje.end_time at time zone 'utc' as end_time ,CASE WHEN bje.STATUS IN ('STARTED','STARTING') AND (bje.create_time at time zone 'utc') < ((CURRENT_TIMESTAMP at time zone 'utc') - interval '1 hours') From bbcba1113e09fd3080cb66640f2d99411f279698 Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 02:01:12 +0100 Subject: [PATCH 19/67] add output representation for job details --- .../ui/controller/JobController.kt | 14 ++- .../integration/ui/dto/JobDetailDto.kt | 102 ++++++++++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/org/ionproject/integration/ui/dto/JobDetailDto.kt diff --git a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt index df6764ce..c3e7f319 100644 --- a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt +++ b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt @@ -3,6 +3,7 @@ package org.ionproject.integration.ui.controller import org.ionproject.integration.application.JobEngine import org.ionproject.integration.ui.dto.CreateJobDto import org.ionproject.integration.ui.dto.InputProcessor +import org.ionproject.integration.ui.dto.JobDetailDto import org.slf4j.LoggerFactory import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -51,8 +52,17 @@ class JobController( fun getJobs(): List = jobEngine.getRunningJobs() @GetMapping("/{id}") - fun getJobDetails(@PathVariable id: Long): JobEngine.IntegrationJob = jobEngine.getJob(id) + fun getJobDetails( + @PathVariable id: Long, + servletRequest: HttpServletRequest, + response: HttpServletResponse + ): JobDetailDto { + val job = jobEngine.getJob(id) + val url = servletRequest.getLocationForJobRequest(job.status) + + return JobDetailDto.of(job, url, JobDetailDto.DetailType.FULL) + } private fun HttpServletRequest.getLocationForJobRequest(jobStatus: JobEngine.JobStatus): String = - "$scheme://$localName:${localPort}$contextPath$JOBS_URI/${jobStatus.jobId}" + "$scheme://$serverName:${localPort}$contextPath$JOBS_URI/${jobStatus.jobId}" } diff --git a/src/main/kotlin/org/ionproject/integration/ui/dto/JobDetailDto.kt b/src/main/kotlin/org/ionproject/integration/ui/dto/JobDetailDto.kt new file mode 100644 index 00000000..9eb76ec9 --- /dev/null +++ b/src/main/kotlin/org/ionproject/integration/ui/dto/JobDetailDto.kt @@ -0,0 +1,102 @@ +package org.ionproject.integration.ui.dto + +import com.fasterxml.jackson.annotation.JsonInclude +import org.ionproject.integration.application.JobEngine +import org.ionproject.integration.domain.common.InstitutionModel +import org.ionproject.integration.domain.common.ProgrammeModel +import org.ionproject.integration.infrastructure.DateUtils +import java.lang.IllegalStateException +import java.net.URI +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime + +@JsonInclude(JsonInclude.Include.NON_NULL) +data class JobDetailDto( + val type: String, + val id: Long, + val status: String, + val createdOn: String, + val startedOn: String? = null, + val endedOn: String? = null, + val links: Links? = null, + val parameters: Parameters? = null +) { + + companion object Factory { + fun of( + job: JobEngine.IntegrationJob, + requestURL: String, + detailType: DetailType = DetailType.FULL + ): JobDetailDto { + + val parameters = if (detailType == DetailType.FULL) Parameters.from(job) else null + + return JobDetailDto( + type = job.type.identifier, + id = job.status.jobId ?: throw IllegalStateException("Job $job cannot have an empty ID field"), + status = job.status.result.name, + createdOn = job.parameters.creationDate.toOutputFormat(), + startedOn = job.parameters.startDate?.toOutputFormat(), + endedOn = job.parameters.endDate?.toOutputFormat(), + links = Links(requestURL), + parameters = parameters + ) + } + + private fun LocalDateTime.toOutputFormat(): String = + DateUtils.formatToISO8601(ZonedDateTime.of(this, ZoneId.systemDefault())) + } + + enum class DetailType { + FULL, METADATA_ONLY + } + + data class Links( + val self: String + ) + + @JsonInclude(JsonInclude.Include.NON_NULL) + data class Parameters( + val format: String, + val institution: Institution, + val programme: Programme? = null, + val sourceUris: List + ) { + + companion object Factory { + fun from(job: JobEngine.IntegrationJob): Parameters { + return Parameters( + format = job.parameters.format.name, + institution = Institution.from(job.parameters.institution), + programme = job.parameters.programme?.let { Programme.from(it) }, + sourceUris = listOf(job.parameters.uri) + ) + } + } + + data class Institution( + val name: String, + val acronym: String, + val identifier: String + ) { + companion object Factory { + fun from(model: InstitutionModel): Institution = + Institution( + name = model.name, + acronym = model.acronym, + identifier = model.identifier + ) + } + } + + data class Programme( + val name: String, + val acronym: String + ) { + companion object Factory { + fun from(model: ProgrammeModel): Programme = Programme(model.name, model.acronym) + } + } + } +} From 841add8a0eaf141d34a4bbdd7924b4976f3fed02 Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 02:04:51 +0100 Subject: [PATCH 20/67] add output representation for running jobs --- .../integration/ui/controller/JobController.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt index c3e7f319..0ae9e731 100644 --- a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt +++ b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt @@ -49,13 +49,17 @@ class JobController( } @GetMapping - fun getJobs(): List = jobEngine.getRunningJobs() + fun getJobs(servletRequest: HttpServletRequest): List = + jobEngine.getRunningJobs() + .map { job -> + val url = servletRequest.getLocationForJobRequest(job.status) + JobDetailDto.of(job, url, JobDetailDto.DetailType.METADATA_ONLY) + } @GetMapping("/{id}") fun getJobDetails( @PathVariable id: Long, - servletRequest: HttpServletRequest, - response: HttpServletResponse + servletRequest: HttpServletRequest ): JobDetailDto { val job = jobEngine.getJob(id) val url = servletRequest.getLocationForJobRequest(job.status) From dae45a33c2d7bce6db291bf62e02d3f458a0c23f Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 02:12:58 +0100 Subject: [PATCH 21/67] add output representation for job creation requests add PostResponse DTO --- .../ui/controller/JobController.kt | 18 ++++++++++++------ .../integration/ui/dto/PostResponse.kt | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/org/ionproject/integration/ui/dto/PostResponse.kt diff --git a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt index 0ae9e731..914f92a7 100644 --- a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt +++ b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt @@ -4,7 +4,9 @@ import org.ionproject.integration.application.JobEngine import org.ionproject.integration.ui.dto.CreateJobDto import org.ionproject.integration.ui.dto.InputProcessor import org.ionproject.integration.ui.dto.JobDetailDto +import org.ionproject.integration.ui.dto.PostResponse import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping @@ -30,20 +32,24 @@ class JobController( @RequestBody body: CreateJobDto, servletRequest: HttpServletRequest, response: HttpServletResponse - ): String { + ): PostResponse { val request = inputProcessor.getJobRequest(body) val requestResult = jobEngine.runJob(request) return when (requestResult.result) { JobEngine.JobExecutionResult.CREATED -> { - response.status = HttpServletResponse.SC_CREATED - response.addHeader("Location", servletRequest.getLocationForJobRequest(requestResult)) - "Created ${request.javaClass.simpleName} job with ID ${requestResult.jobId}" + PostResponse( + location = servletRequest.getLocationForJobRequest(requestResult), + status = HttpStatus.CREATED, + response = response + ) } else -> { logger.error("Job creation failed: $body") - response.status = HttpServletResponse.SC_BAD_REQUEST - "FAILED: ${requestResult.result}" + PostResponse( + status = HttpStatus.BAD_REQUEST, + response = response + ) } } } diff --git a/src/main/kotlin/org/ionproject/integration/ui/dto/PostResponse.kt b/src/main/kotlin/org/ionproject/integration/ui/dto/PostResponse.kt new file mode 100644 index 00000000..468f36fa --- /dev/null +++ b/src/main/kotlin/org/ionproject/integration/ui/dto/PostResponse.kt @@ -0,0 +1,19 @@ +package org.ionproject.integration.ui.dto + +import com.fasterxml.jackson.annotation.JsonInclude +import org.springframework.http.HttpStatus +import javax.servlet.http.HttpServletResponse + +@JsonInclude(JsonInclude.Include.NON_NULL) +class PostResponse( + val location: String? = null, + val status: HttpStatus = HttpStatus.OK, + response: HttpServletResponse +) { + init { + response.status = status.value() + location?.let { + response.addHeader("Location", it) + } + } +} From 12cf83e53c96af78bcd3b692152ab42728885c20 Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 02:25:32 +0100 Subject: [PATCH 22/67] fix unit tests --- .../ionproject/integration/application/JobEngine.kt | 4 ++-- .../integration/ui/controller/JobControllerTests.kt | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt b/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt index 4a1214ca..cb2e2ff0 100644 --- a/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt +++ b/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt @@ -162,8 +162,8 @@ class JobEngine( data class IntegrationJobParameters( val creationDate: LocalDateTime, - val startDate: LocalDateTime?, - val endDate: LocalDateTime?, + val startDate: LocalDateTime? = null, + val endDate: LocalDateTime? = null, val format: OutputFormat, val institution: InstitutionModel, val programme: ProgrammeModel? = null, diff --git a/src/test/kotlin/org/ionproject/integration/ui/controller/JobControllerTests.kt b/src/test/kotlin/org/ionproject/integration/ui/controller/JobControllerTests.kt index 431ffb6a..aec9058f 100644 --- a/src/test/kotlin/org/ionproject/integration/ui/controller/JobControllerTests.kt +++ b/src/test/kotlin/org/ionproject/integration/ui/controller/JobControllerTests.kt @@ -24,6 +24,7 @@ import org.springframework.test.context.TestPropertySource import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @@ -78,7 +79,7 @@ class JobControllerTests { ) val expectedResponse = - """[{"type":"TIMETABLE","status":{"jobId":1,"result":"RUNNING"},"parameters":{"creationDate":"2020-06-30T15:03:00","startDate":"2020-06-30T15:03:00","format":"YAML","institution":{"name":"test","acronym":"test","identifier":"test","academicCalendarUri":"www.test.com"},"programme":null,"uri":"www.test.com"}},{"type":"ACADEMIC_CALENDAR","status":{"jobId":3,"result":"CREATED"},"parameters":{"creationDate":"2020-06-30T15:03:00","startDate":"2020-06-30T15:03:00","format":"YAML","institution":{"name":"test","acronym":"test","identifier":"test","academicCalendarUri":"www.test.com"},"programme":null,"uri":"www.test.com"}}]""" + """[{"type":"timetable","id":1,"status":"RUNNING","createdOn":"2020-06-30T15:03:00Z","startedOn":"2020-06-30T15:03:00Z","links":{"self":"http://localhost:80/integration/jobs/1"}},{"type":"calendar","id":3,"status":"CREATED","createdOn":"2020-06-30T15:03:00Z","startedOn":"2020-06-30T15:03:00Z","links":{"self":"http://localhost:80/integration/jobs/3"}}]""" whenever(jobEngine.getRunningJobs()) doReturn listOf(mockJob1, mockJob2) @@ -97,7 +98,7 @@ class JobControllerTests { ) whenever(jobEngine.runJob(any())) doReturn JobEngine.JobStatus(1, JobEngine.JobExecutionResult.CREATED) - val expectedResponse = "Created CalendarJobRequest job with ID 1" + val expectedResponse = """{"location":"http://localhost:80/integration/jobs/1","status":"CREATED"}""" mockMvc.perform( post("$contextPath$JOBS_URI").contextPath(contextPath) @@ -113,14 +114,17 @@ class JobControllerTests { fun `when receiving a bad job request then return BAD_REQUEST`() { whenever(inputProcessor.getJobRequest(any())) doThrow ArgumentException("You've been a bad, bad boy!") - val expectedResponse = "You've been a bad, bad boy!" + val expectedResponse = + """{"type":"https://github.com/i-on-project/integration/blob/master/docs/infrastructure/ArgumentException.md","title":"Bad Request","status":400,"detail":"You've been a bad, bad boy!","instance":"/integration/jobs"""" mockMvc.perform( post("$contextPath$JOBS_URI").contextPath(contextPath) .contentType(MediaType.APPLICATION_JSON) .content("{}") ) + .andDo(print()) .andExpect(status().isBadRequest) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) .andExpect(content().string(containsString(expectedResponse))) } } From 0df056f7301cac00676b672bb744e2f131f70b57 Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 19:13:37 +0100 Subject: [PATCH 23/67] remove redundant 80 port from generated urls --- .../ionproject/integration/ui/controller/JobController.kt | 8 ++++++-- .../integration/ui/controller/JobControllerTests.kt | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt index 914f92a7..586ea3c9 100644 --- a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt +++ b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt @@ -17,6 +17,7 @@ import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse internal const val JOBS_URI = "/jobs" +private const val HTTP_PORT = 80 @RestController @RequestMapping(JOBS_URI) @@ -73,6 +74,9 @@ class JobController( return JobDetailDto.of(job, url, JobDetailDto.DetailType.FULL) } - private fun HttpServletRequest.getLocationForJobRequest(jobStatus: JobEngine.JobStatus): String = - "$scheme://$serverName:${localPort}$contextPath$JOBS_URI/${jobStatus.jobId}" + private fun HttpServletRequest.getLocationForJobRequest(jobStatus: JobEngine.JobStatus): String { + val portField = if (localPort != HTTP_PORT) ":$localPort" else "" + + return "$scheme://$serverName$portField$contextPath$JOBS_URI/${jobStatus.jobId}" + } } diff --git a/src/test/kotlin/org/ionproject/integration/ui/controller/JobControllerTests.kt b/src/test/kotlin/org/ionproject/integration/ui/controller/JobControllerTests.kt index aec9058f..af6ff2a0 100644 --- a/src/test/kotlin/org/ionproject/integration/ui/controller/JobControllerTests.kt +++ b/src/test/kotlin/org/ionproject/integration/ui/controller/JobControllerTests.kt @@ -79,7 +79,7 @@ class JobControllerTests { ) val expectedResponse = - """[{"type":"timetable","id":1,"status":"RUNNING","createdOn":"2020-06-30T15:03:00Z","startedOn":"2020-06-30T15:03:00Z","links":{"self":"http://localhost:80/integration/jobs/1"}},{"type":"calendar","id":3,"status":"CREATED","createdOn":"2020-06-30T15:03:00Z","startedOn":"2020-06-30T15:03:00Z","links":{"self":"http://localhost:80/integration/jobs/3"}}]""" + """[{"type":"timetable","id":1,"status":"RUNNING","createdOn":"2020-06-30T15:03:00Z","startedOn":"2020-06-30T15:03:00Z","links":{"self":"http://localhost/integration/jobs/1"}},{"type":"calendar","id":3,"status":"CREATED","createdOn":"2020-06-30T15:03:00Z","startedOn":"2020-06-30T15:03:00Z","links":{"self":"http://localhost/integration/jobs/3"}}]""" whenever(jobEngine.getRunningJobs()) doReturn listOf(mockJob1, mockJob2) @@ -98,7 +98,7 @@ class JobControllerTests { ) whenever(jobEngine.runJob(any())) doReturn JobEngine.JobStatus(1, JobEngine.JobExecutionResult.CREATED) - val expectedResponse = """{"location":"http://localhost:80/integration/jobs/1","status":"CREATED"}""" + val expectedResponse = """{"location":"http://localhost/integration/jobs/1","status":"CREATED"}""" mockMvc.perform( post("$contextPath$JOBS_URI").contextPath(contextPath) @@ -106,7 +106,7 @@ class JobControllerTests { .content("{}") ) .andExpect(status().isCreated) - .andExpect(header().string("Location", """http://localhost:80$contextPath$JOBS_URI/1""")) + .andExpect(header().string("Location", """http://localhost$contextPath$JOBS_URI/1""")) .andExpect(content().string(containsString(expectedResponse))) } From 0c1ce62c242348c3b801f57ea2a02b607eb19892 Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 19:21:56 +0100 Subject: [PATCH 24/67] small refactor to DateUtils.kt to allow formatting of LocalDateTime objects --- .../org/ionproject/integration/infrastructure/DateUtils.kt | 7 +++++++ .../org/ionproject/integration/ui/dto/JobDetailDto.kt | 5 +---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/DateUtils.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/DateUtils.kt index 04039eab..aa27553d 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/DateUtils.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/DateUtils.kt @@ -1,7 +1,9 @@ package org.ionproject.integration.infrastructure import java.time.LocalDate +import java.time.LocalDateTime import java.time.Month +import java.time.ZoneId import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @@ -56,6 +58,11 @@ object DateUtils { fun formatToISO8601(zonedDateTime: ZonedDateTime): String = zonedDateTime.format(DateTimeFormatter.ofPattern(CALENDAR_ISO8601_FORMAT)) + fun formatToISO8601(localDateTime: LocalDateTime): String { + val zonedDateTime = ZonedDateTime.of(localDateTime, ZoneId.systemDefault()) + return formatToISO8601(zonedDateTime) + } + fun isDateRange(eventDateString: String): Boolean = eventDateString.contains(PT_DATA_RANGE_DELIMITERS_REGEX.toRegex()) diff --git a/src/main/kotlin/org/ionproject/integration/ui/dto/JobDetailDto.kt b/src/main/kotlin/org/ionproject/integration/ui/dto/JobDetailDto.kt index 9eb76ec9..677a3a15 100644 --- a/src/main/kotlin/org/ionproject/integration/ui/dto/JobDetailDto.kt +++ b/src/main/kotlin/org/ionproject/integration/ui/dto/JobDetailDto.kt @@ -8,8 +8,6 @@ import org.ionproject.integration.infrastructure.DateUtils import java.lang.IllegalStateException import java.net.URI import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZonedDateTime @JsonInclude(JsonInclude.Include.NON_NULL) data class JobDetailDto( @@ -44,8 +42,7 @@ data class JobDetailDto( ) } - private fun LocalDateTime.toOutputFormat(): String = - DateUtils.formatToISO8601(ZonedDateTime.of(this, ZoneId.systemDefault())) + private fun LocalDateTime.toOutputFormat(): String = DateUtils.formatToISO8601(this) } enum class DetailType { From 1bf692520dc4dba21693271c379df8c7aad511d8 Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 22:45:54 +0100 Subject: [PATCH 25/67] change SERVER_PORT variable to PORT --- .env | 2 +- Dockerfile | 4 +--- docker-compose.yml | 4 ++-- src/main/resources/application.properties | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.env b/.env index 3581364f..9e1e367d 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ CONFIG_FILE=/app/config/supported-institutions.yml TEMP_DIR=/app/resources/output STAGING_DIR=/app/staging GIT_PORT=8080 -SERVER_PORT=80 +PORT=80 GIT_SERVER_ADDRESS=http://git-server:8080/git/root/ GIT_REPOSITORY_NAME=integration-data GIT_USER=root diff --git a/Dockerfile b/Dockerfile index ae19102e..e36a3196 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,4 @@ ARG EXTRACT_DEPENDENCY_PATH=/src/build/dependency COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -EXPOSE ${SERVER_PORT} - -ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "-XX:+UseContainerSupport" ] +ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "-XX:+UseContainerSupport", "-DconfigDir=${PORT}" ] diff --git a/docker-compose.yml b/docker-compose.yml index 2c0a2117..4f4614d9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,11 +15,11 @@ services: - GIT_USER=${GIT_USER} - GIT_PASSWORD=${GIT_PASSWORD} - GIT_BRANCH=${GIT_BRANCH} - - SERVER_PORT=${SERVER_PORT} + - PORT=${PORT} volumes: - db-data:/var/lib/postgresql/data ports: - - "${SERVER_PORT}:${SERVER_PORT}" + - "${PORT}:${PORT}" depends_on: ion-db: condition: service_started diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9f3355b1..18807a69 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,7 +3,7 @@ spring.main.banner-mode=console #spring web server.servlet.context-path=/integration -server.port=${SERVER_PORT} +server.port=${PORT} #spring batch spring.datasource.driverClassName=org.postgresql.Driver From 5fa1ba404f5d854822b445a99ddf5bdbc0054012 Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 22:47:58 +0100 Subject: [PATCH 26/67] Update IOnIntegration_PR.yml Co-Authored-By: Ricardo Canto --- .github/workflows/IOnIntegration_PR.yml | 63 +++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/.github/workflows/IOnIntegration_PR.yml b/.github/workflows/IOnIntegration_PR.yml index bb1028ca..5afd9ac2 100644 --- a/.github/workflows/IOnIntegration_PR.yml +++ b/.github/workflows/IOnIntegration_PR.yml @@ -9,13 +9,68 @@ on: - '**/README.md' jobs: - build: + pr_integration: runs-on: ubuntu-latest + if: github.event_name == 'push' && contains(github.ref, 'heads') steps: - name: Checkout uses: actions/checkout@v2 - - name: Build Docker image - id: build_image - run: ./gradlew buildDockerImage -PonlyBuild=true + - name: Setup ACT test environment + if: ${{ env.ACT }} + run: curl https://cli-assets.heroku.com/install-ubuntu.sh | sh + + - name: Retrieve Database URL + id: databaseUrl + run: echo "::set-output name=url::$(heroku config:get DATABASE_URL -a ${{secrets.HEROKU_APP_NAME}})" + env: + HEROKU_API_KEY: ${{secrets.HEROKU_API_KEY}} + + - uses: rishabhgupta/split-by@v1 + id: split1 + with: + string: ${{steps.databaseUrl.outputs.url}} + split-by: '@' + + - uses: rishabhgupta/split-by@v1 + id: split2 + with: + string: ${{steps.split1.outputs._0}} + split-by: ':' + + - uses: rishabhgupta/split-by@v1 + id: split3 + with: + string: ${{steps.split2.outputs._1}} + split-by: '/' + + - uses: akhileshns/heroku-deploy@v3.12.12 + if: ${{ !env.ACT }} + with: + heroku_api_key: ${{secrets.HEROKU_API_KEY}} + heroku_app_name: ${{secrets.HEROKU_APP_NAME}} + heroku_email: ${{secrets.HEROKU_EMAIL}} + usedocker: true + docker_heroku_process_type: web + - run: | + echo "::add-mask::$HD_SQL_HOST" + echo "::add-mask::$HD_SQL_USER" + echo "::add-mask::$HD_SQL_PASSWORD" + env: + HD_SQL_HOST: jdbc:postgresql://${{ steps.split1.outputs._1}} + HD_SQL_USER: ${{ steps.split3.outputs._2}} + HD_SQL_PASSWORD: ${{ steps.split2.outputs._2}} + HD_GIT_PORT: 443 + HD_STAGING_DIR: /app/staging + HD_GIT_SERVER_ADDRESS: https://github.com/i-on-project/ + HD_GIT_REPOSITORY_NAME: integration-data + HD_GIT_USER: ${{secrets.GIT_USER}} + HD_GIT_PASSWORD: "" + HD_GIT_BRANCH: staging + + - name: Print Environment Variables + if: ${{ env.ACT }} + run: | + echo GIT_SERVER_ADDRESS=$GIT_SERVER_ADDRESS + echo GIT_BRANCH=$GIT_BRANCH From 6ff3483999a5ecda8ea64ed31ced01aea775d3c5 Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 22:49:56 +0100 Subject: [PATCH 27/67] Update IOnIntegration_PR.yml Co-Authored-By: Ricardo Canto --- .github/workflows/IOnIntegration_PR.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/IOnIntegration_PR.yml b/.github/workflows/IOnIntegration_PR.yml index 5afd9ac2..a22fff67 100644 --- a/.github/workflows/IOnIntegration_PR.yml +++ b/.github/workflows/IOnIntegration_PR.yml @@ -11,7 +11,6 @@ on: jobs: pr_integration: runs-on: ubuntu-latest - if: github.event_name == 'push' && contains(github.ref, 'heads') steps: - name: Checkout From cffa92ca350e92f006f02e06c1178d3f66af047c Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 22:59:04 +0100 Subject: [PATCH 28/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e36a3196..227b634a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,4 +27,4 @@ ARG EXTRACT_DEPENDENCY_PATH=/src/build/dependency COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "-XX:+UseContainerSupport", "-DconfigDir=${PORT}" ] +ENTRYPOINT java -cp app:app/lib/* org.ionproject.integration.IOnIntegrationApplicationKt -XX:+UseContainerSupport -DconfigDir=$PORT From 0de095d54c43a1b5802a7c572ef53288da2aa56c Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 23:05:24 +0100 Subject: [PATCH 29/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 227b634a..abd71923 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,4 +27,4 @@ ARG EXTRACT_DEPENDENCY_PATH=/src/build/dependency COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -ENTRYPOINT java -cp app:app/lib/* org.ionproject.integration.IOnIntegrationApplicationKt -XX:+UseContainerSupport -DconfigDir=$PORT +ENTRYPOINT "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "-XX:+UseContainerSupport", "-DPORT=", "echo $PORT" ] From 811ea06bd1ccf2d18b7c18ff0ec211b5e92f89d9 Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 23:11:45 +0100 Subject: [PATCH 30/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index abd71923..afdafa2d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,4 +27,4 @@ ARG EXTRACT_DEPENDENCY_PATH=/src/build/dependency COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -ENTRYPOINT "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "-XX:+UseContainerSupport", "-DPORT=", "echo $PORT" ] +ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "-XX:+UseContainerSupport", "-DPORT=", "echo $PORT" ] From 0085c18373e099130a6bd46ec701f934e9103548 Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 23:31:35 +0100 Subject: [PATCH 31/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index afdafa2d..96dfda45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,4 +27,4 @@ ARG EXTRACT_DEPENDENCY_PATH=/src/build/dependency COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "-XX:+UseContainerSupport", "-DPORT=", "echo $PORT" ] +ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "-XX:+UseContainerSupport", "--server.port=", "echo $PORT" ] From 7f643b73108577649c4be8ed16682b0a4cd7aed5 Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 23:36:47 +0100 Subject: [PATCH 32/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 96dfda45..95ff9c97 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,4 +27,4 @@ ARG EXTRACT_DEPENDENCY_PATH=/src/build/dependency COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "-XX:+UseContainerSupport", "--server.port=", "echo $PORT" ] +ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "-XX:+UseContainerSupport", "--server.port=", "$PORT" ] From d59435d83659a4243ad533a50db5e4a31c4ea399 Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 23:43:08 +0100 Subject: [PATCH 33/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 95ff9c97..5b8d2c01 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,4 +27,4 @@ ARG EXTRACT_DEPENDENCY_PATH=/src/build/dependency COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "-XX:+UseContainerSupport", "--server.port=", "$PORT" ] +ENTRYPOINT java -cp "app:app/lib/*" org.ionproject.integration.IOnIntegrationApplicationKt -XX:+UseContainerSupport --server.port=$PORT From 62c877bacb8d44a04129735c4be9408e46d6009d Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 23:48:57 +0100 Subject: [PATCH 34/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5b8d2c01..83f26ad0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,4 +27,6 @@ ARG EXTRACT_DEPENDENCY_PATH=/src/build/dependency COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -ENTRYPOINT java -cp "app:app/lib/*" org.ionproject.integration.IOnIntegrationApplicationKt -XX:+UseContainerSupport --server.port=$PORT +ENV LISTEN_PORT="" + +ENTRYPOINT java -cp "app:app/lib/*" org.ionproject.integration.IOnIntegrationApplicationKt -XX:+UseContainerSupport --server.port=${LISTEN_PORT:-80} From e6d0f4665d7f4fa2988abeb833afa9d89d3d8ca5 Mon Sep 17 00:00:00 2001 From: Grimord Date: Mon, 5 Jul 2021 23:54:12 +0100 Subject: [PATCH 35/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 83f26ad0..51a5256c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,5 @@ ARG EXTRACT_DEPENDENCY_PATH=/src/build/dependency COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -ENV LISTEN_PORT="" -ENTRYPOINT java -cp "app:app/lib/*" org.ionproject.integration.IOnIntegrationApplicationKt -XX:+UseContainerSupport --server.port=${LISTEN_PORT:-80} +ENTRYPOINT java -cp "app:app/lib/*" org.ionproject.integration.IOnIntegrationApplicationKt --server.port=$PORT From 529550d9fdcf060d8edcb742e09fcbbe55441f5a Mon Sep 17 00:00:00 2001 From: Grimord Date: Tue, 6 Jul 2021 00:05:43 +0100 Subject: [PATCH 36/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 51a5256c..7184fcf2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,4 +28,4 @@ COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -ENTRYPOINT java -cp "app:app/lib/*" org.ionproject.integration.IOnIntegrationApplicationKt --server.port=$PORT +ENTRYPOINT java -cp "app:app/lib/*" org.ionproject.integration.IOnIntegrationApplicationKt --server.port $PORT From 0226ac5755add682420709bb89843b35e9666938 Mon Sep 17 00:00:00 2001 From: Grimord Date: Tue, 6 Jul 2021 00:11:12 +0100 Subject: [PATCH 37/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7184fcf2..0d90dd03 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,4 +28,4 @@ COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -ENTRYPOINT java -cp "app:app/lib/*" org.ionproject.integration.IOnIntegrationApplicationKt --server.port $PORT +ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "--server.port", "echo $PORT" ] From d457455ae8c183f96b0c7f6c41d928a58a0be75a Mon Sep 17 00:00:00 2001 From: Grimord Date: Tue, 6 Jul 2021 00:17:35 +0100 Subject: [PATCH 38/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0d90dd03..a0d4e0b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,4 +28,4 @@ COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "--server.port", "echo $PORT" ] +ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "--server.port", "$PORT" ] From c701b76c11b84a6c673566f92c7e041264add8e4 Mon Sep 17 00:00:00 2001 From: Grimord Date: Tue, 6 Jul 2021 00:24:27 +0100 Subject: [PATCH 39/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a0d4e0b4..1ad15e14 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,4 +28,4 @@ COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "--server.port", "$PORT" ] +ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "-Dserver.port", "$PORT" ] From bed702e40d4465d8aefa5b5047f95244967a655a Mon Sep 17 00:00:00 2001 From: Grimord Date: Tue, 6 Jul 2021 00:34:32 +0100 Subject: [PATCH 40/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1ad15e14..84defaf1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,5 +27,7 @@ ARG EXTRACT_DEPENDENCY_PATH=/src/build/dependency COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib +ENV SERVER_PORT=$PORT -ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "-Dserver.port", "$PORT" ] + +ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "--server.port=${SERVER_PORT}"] From 7915376f0fd8ba177a2d81ce6444444a9539c9bc Mon Sep 17 00:00:00 2001 From: Grimord Date: Tue, 6 Jul 2021 00:39:41 +0100 Subject: [PATCH 41/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 84defaf1..72bc5656 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,4 +30,4 @@ COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib ENV SERVER_PORT=$PORT -ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "--server.port=${SERVER_PORT}"] +ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "--server.port" , "echo $SERVER_PORT"] From 95d0da6b7b3d765e4d4ef4397ce988dd49f09614 Mon Sep 17 00:00:00 2001 From: Grimord Date: Tue, 6 Jul 2021 00:45:56 +0100 Subject: [PATCH 42/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 72bc5656..489a6260 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,5 @@ ARG EXTRACT_DEPENDENCY_PATH=/src/build/dependency COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -ENV SERVER_PORT=$PORT - -ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "--server.port" , "echo $SERVER_PORT"] +ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "--server.port" , $PORT] From 4e4d9b574aa2cc3b7fcf86fcdc547a937e6e807e Mon Sep 17 00:00:00 2001 From: Grimord Date: Tue, 6 Jul 2021 00:53:18 +0100 Subject: [PATCH 43/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 489a6260..0533fb80 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,4 +28,4 @@ COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "--server.port" , $PORT] +ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "--server.port" , "echo $PORT" ] From 7457aa3f9ff5813b23b056f9479db1f8322d923b Mon Sep 17 00:00:00 2001 From: Grimord Date: Tue, 6 Jul 2021 00:59:05 +0100 Subject: [PATCH 44/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0533fb80..6d1bcb4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,4 +28,4 @@ COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "--server.port" , "echo $PORT" ] +ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "echo --server.port=$PORT" ] From 640485d573a6167bfb0f69aaa84d7ddd7e0f3d6c Mon Sep 17 00:00:00 2001 From: Grimord Date: Tue, 6 Jul 2021 01:11:48 +0100 Subject: [PATCH 45/67] Update Dockerfile Co-Authored-By: Ricardo Canto --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6d1bcb4f..f05ac6f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,4 +28,4 @@ COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/classes /app COPY --from=build-env ${EXTRACT_DEPENDENCY_PATH}/BOOT-INF/lib /app/lib -ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "echo --server.port=$PORT" ] +ENTRYPOINT [ "java", "-cp", "app:app/lib/*", "org.ionproject.integration.IOnIntegrationApplicationKt", "-XX:+UseContainerSupport", "echo --server.port=$PORT" ] From d1cd8cd13e4f4d2bfd9090cc75418cc4eef943d7 Mon Sep 17 00:00:00 2001 From: Grimord Date: Tue, 6 Jul 2021 01:30:40 +0100 Subject: [PATCH 46/67] fix env file fix docker compose change controller port to server port --- .env | 2 +- docker-compose.yml | 4 ++-- .../org/ionproject/integration/ui/controller/JobController.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 9e1e367d..bb4bea0b 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ CONFIG_FILE=/app/config/supported-institutions.yml TEMP_DIR=/app/resources/output STAGING_DIR=/app/staging GIT_PORT=8080 -PORT=80 +SERVER_PORT=889 GIT_SERVER_ADDRESS=http://git-server:8080/git/root/ GIT_REPOSITORY_NAME=integration-data GIT_USER=root diff --git a/docker-compose.yml b/docker-compose.yml index 4f4614d9..38f78914 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,11 +15,11 @@ services: - GIT_USER=${GIT_USER} - GIT_PASSWORD=${GIT_PASSWORD} - GIT_BRANCH=${GIT_BRANCH} - - PORT=${PORT} + - PORT=${SERVER_PORT} volumes: - db-data:/var/lib/postgresql/data ports: - - "${PORT}:${PORT}" + - "${SERVER_PORT}:${SERVER_PORT}" depends_on: ion-db: condition: service_started diff --git a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt index 586ea3c9..46126b08 100644 --- a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt +++ b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt @@ -75,7 +75,7 @@ class JobController( } private fun HttpServletRequest.getLocationForJobRequest(jobStatus: JobEngine.JobStatus): String { - val portField = if (localPort != HTTP_PORT) ":$localPort" else "" + val portField = if (serverPort != HTTP_PORT) ":$serverPort" else "" return "$scheme://$serverName$portField$contextPath$JOBS_URI/${jobStatus.jobId}" } From 481a15cb256abbf30bd71f129e7cfc3ae14c8cf6 Mon Sep 17 00:00:00 2001 From: Grimord Date: Tue, 6 Jul 2021 01:37:38 +0100 Subject: [PATCH 47/67] revert back PR pipeline --- .github/workflows/IOnIntegration_PR.yml | 62 ++----------------------- 1 file changed, 4 insertions(+), 58 deletions(-) diff --git a/.github/workflows/IOnIntegration_PR.yml b/.github/workflows/IOnIntegration_PR.yml index a22fff67..bb1028ca 100644 --- a/.github/workflows/IOnIntegration_PR.yml +++ b/.github/workflows/IOnIntegration_PR.yml @@ -9,67 +9,13 @@ on: - '**/README.md' jobs: - pr_integration: + build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - - name: Setup ACT test environment - if: ${{ env.ACT }} - run: curl https://cli-assets.heroku.com/install-ubuntu.sh | sh - - - name: Retrieve Database URL - id: databaseUrl - run: echo "::set-output name=url::$(heroku config:get DATABASE_URL -a ${{secrets.HEROKU_APP_NAME}})" - env: - HEROKU_API_KEY: ${{secrets.HEROKU_API_KEY}} - - - uses: rishabhgupta/split-by@v1 - id: split1 - with: - string: ${{steps.databaseUrl.outputs.url}} - split-by: '@' - - - uses: rishabhgupta/split-by@v1 - id: split2 - with: - string: ${{steps.split1.outputs._0}} - split-by: ':' - - - uses: rishabhgupta/split-by@v1 - id: split3 - with: - string: ${{steps.split2.outputs._1}} - split-by: '/' - - - uses: akhileshns/heroku-deploy@v3.12.12 - if: ${{ !env.ACT }} - with: - heroku_api_key: ${{secrets.HEROKU_API_KEY}} - heroku_app_name: ${{secrets.HEROKU_APP_NAME}} - heroku_email: ${{secrets.HEROKU_EMAIL}} - usedocker: true - docker_heroku_process_type: web - - run: | - echo "::add-mask::$HD_SQL_HOST" - echo "::add-mask::$HD_SQL_USER" - echo "::add-mask::$HD_SQL_PASSWORD" - env: - HD_SQL_HOST: jdbc:postgresql://${{ steps.split1.outputs._1}} - HD_SQL_USER: ${{ steps.split3.outputs._2}} - HD_SQL_PASSWORD: ${{ steps.split2.outputs._2}} - HD_GIT_PORT: 443 - HD_STAGING_DIR: /app/staging - HD_GIT_SERVER_ADDRESS: https://github.com/i-on-project/ - HD_GIT_REPOSITORY_NAME: integration-data - HD_GIT_USER: ${{secrets.GIT_USER}} - HD_GIT_PASSWORD: "" - HD_GIT_BRANCH: staging - - - name: Print Environment Variables - if: ${{ env.ACT }} - run: | - echo GIT_SERVER_ADDRESS=$GIT_SERVER_ADDRESS - echo GIT_BRANCH=$GIT_BRANCH + - name: Build Docker image + id: build_image + run: ./gradlew buildDockerImage -PonlyBuild=true From 3ccb25773443806be8f82755f6d313ec2ec1d843 Mon Sep 17 00:00:00 2001 From: Grimord Date: Tue, 6 Jul 2021 01:39:30 +0100 Subject: [PATCH 48/67] revert .env local port map --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index bb4bea0b..3581364f 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ CONFIG_FILE=/app/config/supported-institutions.yml TEMP_DIR=/app/resources/output STAGING_DIR=/app/staging GIT_PORT=8080 -SERVER_PORT=889 +SERVER_PORT=80 GIT_SERVER_ADDRESS=http://git-server:8080/git/root/ GIT_REPOSITORY_NAME=integration-data GIT_USER=root From eef82b9f500f55aad70db34018418024bc55a2e3 Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Sun, 27 Jun 2021 18:07:26 +0100 Subject: [PATCH 49/67] Rebase from master --- .../integration/application/JobEngine.kt | 6 +- .../job/ISELAcademicCalendarJob.kt | 1 + .../application/job/ISELEvaluationsJob.kt | 147 ++++++++++++++++++ .../application/job/NotificationListener.kt | 24 +++ .../domain/calendar/OutputRepresentations.kt | 2 +- .../domain/common/dto/SchoolDto.kt | 6 + .../domain/evaluations/BusinessObjects.kt | 46 ++++++ .../evaluations/OutputRepresentations.kt | 34 ++++ .../domain/evaluations/RawEvaluationsData.kt | 7 + .../timetable/dto/OutputRepresentations.kt | 6 +- .../AcademicCalendarDtoFormatCheckerTest.kt | 2 +- 11 files changed, 271 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt create mode 100644 src/main/kotlin/org/ionproject/integration/application/job/NotificationListener.kt create mode 100644 src/main/kotlin/org/ionproject/integration/domain/common/dto/SchoolDto.kt create mode 100644 src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt create mode 100644 src/main/kotlin/org/ionproject/integration/domain/evaluations/OutputRepresentations.kt create mode 100644 src/main/kotlin/org/ionproject/integration/domain/evaluations/RawEvaluationsData.kt diff --git a/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt b/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt index cb2e2ff0..2815eec2 100644 --- a/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt +++ b/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt @@ -2,7 +2,7 @@ package org.ionproject.integration.application import org.ionproject.integration.application.config.AppProperties import org.ionproject.integration.application.config.LAUNCHER_NAME -import org.ionproject.integration.application.job.CALENDAR_JOB_NAME +import org.ionproject.integration.application.job.EVALUATIONS_JOB_NAME import org.ionproject.integration.infrastructure.file.OutputFormat import org.ionproject.integration.domain.common.InstitutionModel import org.ionproject.integration.domain.common.ProgrammeModel @@ -63,8 +63,8 @@ class JobEngine( } private fun runCalendarJob(request: CalendarJobRequest): JobStatus { - val jobParams = getJobParameters(request, CALENDAR_JOB_NAME) - return runJob(CALENDAR_JOB_NAME, jobParams) + val jobParams = getJobParameters(request, EVALUATIONS_JOB_NAME) + return runJob(EVALUATIONS_JOB_NAME, jobParams) } private fun getJobParameters(request: AbstractJobRequest, jobName: String): JobParameters { diff --git a/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt b/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt index fa4220ff..9fa5b8ce 100644 --- a/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt +++ b/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt @@ -19,6 +19,7 @@ import org.ionproject.integration.infrastructure.pdfextractor.AcademicCalendarEx import org.ionproject.integration.infrastructure.pdfextractor.ITextPdfExtractor import org.ionproject.integration.infrastructure.pdfextractor.PDFBytesFormatChecker import org.ionproject.integration.model.external.calendar.AcademicCalendar +import org.ionproject.integration.model.external.calendar.AcademicCalendarDto import org.springframework.batch.core.ExitStatus import org.springframework.batch.core.configuration.annotation.JobBuilderFactory import org.springframework.batch.core.configuration.annotation.StepBuilderFactory diff --git a/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt b/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt new file mode 100644 index 00000000..d0ac7cbf --- /dev/null +++ b/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt @@ -0,0 +1,147 @@ +package org.ionproject.integration.application.job + +import org.ionproject.integration.application.config.AppProperties +import org.ionproject.integration.application.dispatcher.IDispatcher +import org.ionproject.integration.application.job.tasklet.DownloadAndCompareTasklet +import org.ionproject.integration.domain.evaluations.Evaluations +import org.ionproject.integration.domain.evaluations.EvaluationsDto +import org.ionproject.integration.domain.evaluations.RawEvaluationsData +import org.ionproject.integration.infrastructure.Try +import org.ionproject.integration.infrastructure.file.FileComparatorImpl +import org.ionproject.integration.infrastructure.file.FileDigestImpl +import org.ionproject.integration.infrastructure.hash.HashRepositoryImpl +import org.ionproject.integration.infrastructure.http.IFileDownloader +import org.ionproject.integration.infrastructure.orThrow +import org.ionproject.integration.infrastructure.pdfextractor.AcademicCalendarExtractor +import org.ionproject.integration.infrastructure.pdfextractor.ITextPdfExtractor +import org.ionproject.integration.infrastructure.pdfextractor.PDFBytesFormatChecker +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory +import org.springframework.batch.core.configuration.annotation.StepScope +import org.springframework.batch.core.step.tasklet.Tasklet +import org.springframework.batch.core.step.tasklet.TaskletStep +import org.springframework.batch.repeat.RepeatStatus +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.stereotype.Component +import java.io.File +import javax.sql.DataSource + +const val EVALUATIONS_JOB_NAME = "evaluations" + +@Configuration +class ISELEvaluationsJob( + val jobBuilderFactory: JobBuilderFactory, + val stepBuilderFactory: StepBuilderFactory, + val properties: AppProperties, + val downloader: IFileDownloader, + val dispatcher: IDispatcher, + @Autowired + val ds: DataSource +) { + + @Bean(name = [EVALUATIONS_JOB_NAME]) + fun calendarJob() = jobBuilderFactory.get(EVALUATIONS_JOB_NAME) + .start(taskletStep("Download And Compare", downloadEvaluationsPDFTasklet())) + .on("STOPPED").end() + .next(extractEvaluationsPDFTasklet()) + // .next(createEvaluationsPDFBusinessObjectsTasklet()) + // .next(createEvaluationsPDFDtoTasklet()) + // .next(writeEvaluationsDTOToGitTasklet()) + .build().listener(NotificationListener()) + .build() + + private fun taskletStep(name: String, tasklet: Tasklet): TaskletStep { + return stepBuilderFactory + .get(name) + .tasklet(tasklet) + .build() + } + + @StepScope + @Bean + fun downloadEvaluationsPDFTasklet(): DownloadAndCompareTasklet { + val pdfChecker = PDFBytesFormatChecker() + val fileComparator = FileComparatorImpl(FileDigestImpl(), HashRepositoryImpl(ds)) + return DownloadAndCompareTasklet(downloader, pdfChecker, fileComparator) + } + +/* @Bean + fun downloadEvaluationsPDFTasklet() = stepBuilderFactory.get("Download Calendar PDF") + .tasklet { stepContribution, chunkContext -> + val pdfChecker = PDFBytesFormatChecker() + val fileComparator = FileComparatorImpl(FileDigestImpl(), HashRepositoryImpl(ds)) + DownloadAndCompareTasklet(downloader, pdfChecker, fileComparator).execute(stepContribution, chunkContext) + } + .build()*/ + + @Bean + fun extractEvaluationsPDFTasklet() = stepBuilderFactory.get("Extract Evaluations PDF Raw Data") + .tasklet { stepContribution, _ -> + val path = stepContribution.stepExecution.jobExecution.executionContext.get("file-path").toString() + + State.rawEvaluationsData = extractEvaluationsPDF(path) + RepeatStatus.FINISHED + } + .build() + + fun extractEvaluationsPDF(path: String): RawEvaluationsData { + try { + val itext = ITextPdfExtractor() + + val headerText = itext.extract(path) + val evaluationsTable = AcademicCalendarExtractor.calendarTable.extract(path) + + return Try.map( + headerText, + evaluationsTable + ) { (text, evaluationsTable) -> + RawEvaluationsData( + text.dropLast(1), + evaluationsTable.first().replace("\\r", " "), + text.last() + ) + }.orThrow() + } finally { + File(path).delete() + } + } + + @Bean + fun createEvaluationsPDFBusinessObjectsTasklet() = + stepBuilderFactory.get("Create Business Objects from Evaluations Raw Data") + .tasklet { _, _ -> + State.evaluations = Evaluations.from(State.rawEvaluationsData) + RepeatStatus.FINISHED + } + .build() + + @Bean + fun createEvaluationsPDFDtoTasklet() = stepBuilderFactory.get("Create DTO from Evaluations Business Objects") + .tasklet { _, _ -> + State.evaluationsDto = EvaluationsDto.from(State.evaluations) + RepeatStatus.FINISHED + } + .build() + + // TODO + @Bean + fun writeEvaluationsDTOToGitTasklet() = stepBuilderFactory.get("Write Calendar DTO to Git") + .tasklet { _, _ -> +/* dispatcher.dispatch( + EvaluationsData.from(State.evaluationsDto), + EVALUATIONS_JOB_NAME, + OutputFormat.JSON + )*/ + RepeatStatus.FINISHED + } + .build() + + @Component + object State { + lateinit var rawEvaluationsData: RawEvaluationsData + lateinit var evaluations: Evaluations + lateinit var evaluationsDto: EvaluationsDto + } +} diff --git a/src/main/kotlin/org/ionproject/integration/application/job/NotificationListener.kt b/src/main/kotlin/org/ionproject/integration/application/job/NotificationListener.kt new file mode 100644 index 00000000..b447dc97 --- /dev/null +++ b/src/main/kotlin/org/ionproject/integration/application/job/NotificationListener.kt @@ -0,0 +1,24 @@ +package org.ionproject.integration.application.job + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.batch.core.ExitStatus +import org.springframework.batch.core.JobExecution +import org.springframework.batch.core.JobExecutionListener + +class NotificationListener : JobExecutionListener { + + private val log: Logger = LoggerFactory.getLogger(NotificationListener::class.java) + + override fun beforeJob(jobExecution: JobExecution) { + log.info("Job ${jobExecution.jobConfigurationName} starting") + } + + override fun afterJob(jobExecution: JobExecution) { + when (jobExecution.exitStatus) { + ExitStatus.FAILED -> log.error("Job ${jobExecution.jobConfigurationName} failed") + ExitStatus.COMPLETED -> log.info("Job ${jobExecution.jobConfigurationName} completed") + else -> log.debug("Job ${jobExecution.jobConfigurationName} exited with status = ${jobExecution.status}") + } + } +} diff --git a/src/main/kotlin/org/ionproject/integration/domain/calendar/OutputRepresentations.kt b/src/main/kotlin/org/ionproject/integration/domain/calendar/OutputRepresentations.kt index 7247cdc9..f81916d2 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/calendar/OutputRepresentations.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/calendar/OutputRepresentations.kt @@ -1,6 +1,6 @@ package org.ionproject.integration.domain.calendar -import org.ionproject.integration.domain.timetable.dto.SchoolDto +import org.ionproject.integration.domain.common.dto.SchoolDto import org.ionproject.integration.infrastructure.DateUtils import org.ionproject.integration.model.external.calendar.AcademicCalendar import org.ionproject.integration.model.external.calendar.Lectures diff --git a/src/main/kotlin/org/ionproject/integration/domain/common/dto/SchoolDto.kt b/src/main/kotlin/org/ionproject/integration/domain/common/dto/SchoolDto.kt new file mode 100644 index 00000000..95cc7bd2 --- /dev/null +++ b/src/main/kotlin/org/ionproject/integration/domain/common/dto/SchoolDto.kt @@ -0,0 +1,6 @@ +package org.ionproject.integration.domain.common.dto + +data class SchoolDto( + val name: String, + val acr: String, +) diff --git a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt new file mode 100644 index 00000000..d8816277 --- /dev/null +++ b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt @@ -0,0 +1,46 @@ +package org.ionproject.integration.domain.evaluations + +import org.ionproject.integration.domain.common.School +import org.ionproject.integration.infrastructure.DateUtils +import java.time.LocalDateTime +import java.time.ZonedDateTime + +data class Evaluations( + val creationDateTime: String = "", + val retrievalDateTime: String = "", + val school: School = School(), + val calendarTerm: String = "", + val exams: List +) { + companion object { + fun from(rawEvaluationsData: RawEvaluationsData): Evaluations = + Evaluations( + creationDateTime = rawEvaluationsData.creationDate, + retrievalDateTime = DateUtils.formatToISO8601(ZonedDateTime.now()), + School( + "Instituto Superior de Engenharia de Lisboa", + "ISEL" + ), + calendarTerm = buildCalendarTerm(rawEvaluationsData), + emptyList() + ) + + // TODO + private fun buildCalendarTerm(rawEvaluationsData: RawEvaluationsData): String = "" + } +} + +data class Exam( + val course: String, + val startDate: LocalDateTime, + val endDate: LocalDateTime, + val category: ExamCategory, + val location: String +) + +enum class ExamCategory { + TEST, + EXAM_NORMAL, + EXAM_ALTERN, + EXAM_SPECIAL +} diff --git a/src/main/kotlin/org/ionproject/integration/domain/evaluations/OutputRepresentations.kt b/src/main/kotlin/org/ionproject/integration/domain/evaluations/OutputRepresentations.kt new file mode 100644 index 00000000..c7aaecd2 --- /dev/null +++ b/src/main/kotlin/org/ionproject/integration/domain/evaluations/OutputRepresentations.kt @@ -0,0 +1,34 @@ +package org.ionproject.integration.domain.evaluations + +import org.ionproject.integration.domain.common.dto.SchoolDto + +data class EvaluationsDto( + val creationDateTime: String = "", + val retrievalDateTime: String = "", + val school: SchoolDto, + val calendarTerm: String, + val exams: List +) { + companion object { + fun from(evaluations: Evaluations): EvaluationsDto { + return EvaluationsDto( + evaluations.creationDateTime, + evaluations.retrievalDateTime, + SchoolDto( + evaluations.school.name, + evaluations.school.acr + ), + evaluations.calendarTerm, + emptyList() + ) + } + } +} + +data class ExamDto( + val course: String, + val startDate: String, + val endDate: String, + val category: String, + val location: String +) diff --git a/src/main/kotlin/org/ionproject/integration/domain/evaluations/RawEvaluationsData.kt b/src/main/kotlin/org/ionproject/integration/domain/evaluations/RawEvaluationsData.kt new file mode 100644 index 00000000..01d85b61 --- /dev/null +++ b/src/main/kotlin/org/ionproject/integration/domain/evaluations/RawEvaluationsData.kt @@ -0,0 +1,7 @@ +package org.ionproject.integration.domain.evaluations + +data class RawEvaluationsData( + val textData: List, + val table: String, + val creationDate: String +) diff --git a/src/main/kotlin/org/ionproject/integration/domain/timetable/dto/OutputRepresentations.kt b/src/main/kotlin/org/ionproject/integration/domain/timetable/dto/OutputRepresentations.kt index 03fe383d..e0622720 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/timetable/dto/OutputRepresentations.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/timetable/dto/OutputRepresentations.kt @@ -1,6 +1,7 @@ package org.ionproject.integration.domain.timetable.dto import com.fasterxml.jackson.annotation.JsonInclude +import org.ionproject.integration.domain.common.dto.SchoolDto import org.ionproject.integration.domain.timetable.model.Course import org.ionproject.integration.domain.timetable.model.CourseTeacher import org.ionproject.integration.domain.timetable.model.EventCategory @@ -83,11 +84,6 @@ data class TimetableDto( } } -data class SchoolDto( - val name: String, - val acr: String, -) - data class ProgrammeDto( val name: String, val acr: String, diff --git a/src/test/kotlin/org/ionproject/integration/format/implementations/AcademicCalendarDtoFormatCheckerTest.kt b/src/test/kotlin/org/ionproject/integration/format/implementations/AcademicCalendarDtoFormatCheckerTest.kt index 95999826..96ffc2a4 100644 --- a/src/test/kotlin/org/ionproject/integration/format/implementations/AcademicCalendarDtoFormatCheckerTest.kt +++ b/src/test/kotlin/org/ionproject/integration/format/implementations/AcademicCalendarDtoFormatCheckerTest.kt @@ -11,7 +11,7 @@ import org.ionproject.integration.model.external.calendar.Term import org.ionproject.integration.domain.calendar.TermDto import org.ionproject.integration.domain.common.Language import org.ionproject.integration.domain.common.School -import org.ionproject.integration.domain.timetable.dto.SchoolDto +import org.ionproject.integration.domain.common.dto.SchoolDto import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import java.time.LocalDate From 902a24ac6145b115b5f35ff3662abf6063ddf4ee Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Sat, 26 Jun 2021 19:48:49 +0100 Subject: [PATCH 50/67] Setting the structure for EvaluationsJob --- .../integration/application/job/ISELEvaluationsJob.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt b/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt index d0ac7cbf..d66a29fa 100644 --- a/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt +++ b/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt @@ -1,5 +1,6 @@ package org.ionproject.integration.application.job +import org.ionproject.integration.application.JobEngine import org.ionproject.integration.application.config.AppProperties import org.ionproject.integration.application.dispatcher.IDispatcher import org.ionproject.integration.application.job.tasklet.DownloadAndCompareTasklet @@ -78,9 +79,9 @@ class ISELEvaluationsJob( @Bean fun extractEvaluationsPDFTasklet() = stepBuilderFactory.get("Extract Evaluations PDF Raw Data") - .tasklet { stepContribution, _ -> + .tasklet { stepContribution, context -> val path = stepContribution.stepExecution.jobExecution.executionContext.get("file-path").toString() - + val school = context.stepContext.jobParameters[JobEngine.INSTITUTION_PARAMETER] as String State.rawEvaluationsData = extractEvaluationsPDF(path) RepeatStatus.FINISHED } From 74f1a7c1d36e27d80fefcb0487cb8d2f6c717dcc Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Sun, 27 Jun 2021 02:19:04 +0100 Subject: [PATCH 51/67] Controller and JobEngine updated for Evaluations Job --- .../integration/application/JobEngine.kt | 44 ++++++++++++++++--- .../application/job/ISELEvaluationsJob.kt | 4 +- .../integration/application/job/JobType.kt | 2 +- .../domain/common/ProgrammeModel.kt | 4 +- .../domain/common/ProgrammeResources.kt | 8 ++++ .../pdfextractor/EvaluationsExtractor.kt | 17 +++++++ .../integration/ui/dto/CreateJobDto.kt | 3 +- .../ui/dto/SafeEvaluationsJobDto.kt | 23 ++++++++++ .../integration/dispatcher/FileWriterTests.kt | 2 +- .../TimetableDtoFormatCheckerTest.kt | 2 +- .../implementations/WriteFileTaskletTests.kt | 2 +- 11 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 src/main/kotlin/org/ionproject/integration/domain/common/ProgrammeResources.kt create mode 100644 src/main/kotlin/org/ionproject/integration/infrastructure/pdfextractor/EvaluationsExtractor.kt create mode 100644 src/main/kotlin/org/ionproject/integration/ui/dto/SafeEvaluationsJobDto.kt diff --git a/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt b/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt index 2815eec2..49e7760c 100644 --- a/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt +++ b/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt @@ -3,13 +3,13 @@ package org.ionproject.integration.application import org.ionproject.integration.application.config.AppProperties import org.ionproject.integration.application.config.LAUNCHER_NAME import org.ionproject.integration.application.job.EVALUATIONS_JOB_NAME -import org.ionproject.integration.infrastructure.file.OutputFormat -import org.ionproject.integration.domain.common.InstitutionModel -import org.ionproject.integration.domain.common.ProgrammeModel -import org.ionproject.integration.infrastructure.repository.IIntegrationJobRepository import org.ionproject.integration.application.job.JobType import org.ionproject.integration.application.job.TIMETABLE_JOB_NAME +import org.ionproject.integration.domain.common.InstitutionModel +import org.ionproject.integration.domain.common.ProgrammeModel import org.ionproject.integration.infrastructure.exception.JobNotFoundException +import org.ionproject.integration.infrastructure.file.OutputFormat +import org.ionproject.integration.infrastructure.repository.IIntegrationJobRepository import org.slf4j.LoggerFactory import org.springframework.batch.core.Job import org.springframework.batch.core.JobParameters @@ -49,6 +49,7 @@ class JobEngine( fun runJob(request: AbstractJobRequest): JobStatus { return when (request) { is TimetableJobRequest -> runTimetableJob(request) + is EvaluationsJobRequest -> runEvaluationsJob(request) is CalendarJobRequest -> runCalendarJob(request) } } @@ -62,6 +63,11 @@ class JobEngine( return runJob(TIMETABLE_JOB_NAME, jobParams) } + private fun runEvaluationsJob(request: EvaluationsJobRequest): JobStatus { + val jobParams = getJobParameters(request, EVALUATIONS_JOB_NAME) + return runJob(EVALUATIONS_JOB_NAME, jobParams) + } + private fun runCalendarJob(request: CalendarJobRequest): JobStatus { val jobParams = getJobParameters(request, EVALUATIONS_JOB_NAME) return runJob(EVALUATIONS_JOB_NAME, jobParams) @@ -72,7 +78,11 @@ class JobEngine( val uri = when (request) { is TimetableJobRequest -> - request.programme.timetableUri.also { + request.programme.resources.timetableUri.also { + parametersBuilder.addString(PROGRAMME_PARAMETER, request.programme.acronym) + } + is EvaluationsJobRequest -> + request.programme.resources.evaluationsUri.also { parametersBuilder.addString(PROGRAMME_PARAMETER, request.programme.acronym) } is CalendarJobRequest -> request.institution.academicCalendarUri @@ -154,6 +164,30 @@ class JobEngine( } } + class EvaluationsJobRequest( + format: OutputFormat, + institution: InstitutionModel, + val programme: ProgrammeModel + ) : AbstractJobRequest(format, institution, JobType.EXAM_SCHEDULE) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as EvaluationsJobRequest + + if (programme != other.programme) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + programme.hashCode() + return result + } + } + data class IntegrationJob( val type: JobType, val status: JobStatus, diff --git a/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt b/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt index d66a29fa..11cce47a 100644 --- a/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt +++ b/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt @@ -13,7 +13,7 @@ import org.ionproject.integration.infrastructure.file.FileDigestImpl import org.ionproject.integration.infrastructure.hash.HashRepositoryImpl import org.ionproject.integration.infrastructure.http.IFileDownloader import org.ionproject.integration.infrastructure.orThrow -import org.ionproject.integration.infrastructure.pdfextractor.AcademicCalendarExtractor +import org.ionproject.integration.infrastructure.pdfextractor.EvaluationsExtractor import org.ionproject.integration.infrastructure.pdfextractor.ITextPdfExtractor import org.ionproject.integration.infrastructure.pdfextractor.PDFBytesFormatChecker import org.springframework.batch.core.configuration.annotation.JobBuilderFactory @@ -92,7 +92,7 @@ class ISELEvaluationsJob( val itext = ITextPdfExtractor() val headerText = itext.extract(path) - val evaluationsTable = AcademicCalendarExtractor.calendarTable.extract(path) + val evaluationsTable = EvaluationsExtractor.evaluationsTable.extract(path) return Try.map( headerText, diff --git a/src/main/kotlin/org/ionproject/integration/application/job/JobType.kt b/src/main/kotlin/org/ionproject/integration/application/job/JobType.kt index 6a96a17f..73a49804 100644 --- a/src/main/kotlin/org/ionproject/integration/application/job/JobType.kt +++ b/src/main/kotlin/org/ionproject/integration/application/job/JobType.kt @@ -2,7 +2,7 @@ package org.ionproject.integration.application.job enum class JobType(val identifier: String) { TIMETABLE("timetable"), - EXAM_SCHEDULE("evaluation"), + EXAM_SCHEDULE("evaluations"), ACADEMIC_CALENDAR("calendar"); companion object Factory { diff --git a/src/main/kotlin/org/ionproject/integration/domain/common/ProgrammeModel.kt b/src/main/kotlin/org/ionproject/integration/domain/common/ProgrammeModel.kt index fcbcec3e..8ab21646 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/common/ProgrammeModel.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/common/ProgrammeModel.kt @@ -1,10 +1,8 @@ package org.ionproject.integration.domain.common -import java.net.URI - data class ProgrammeModel( val institutionModel: InstitutionModel, val name: String, val acronym: String, - val timetableUri: URI + val resources: ProgrammeResources ) diff --git a/src/main/kotlin/org/ionproject/integration/domain/common/ProgrammeResources.kt b/src/main/kotlin/org/ionproject/integration/domain/common/ProgrammeResources.kt new file mode 100644 index 00000000..77062270 --- /dev/null +++ b/src/main/kotlin/org/ionproject/integration/domain/common/ProgrammeResources.kt @@ -0,0 +1,8 @@ +package org.ionproject.integration.domain.common + +import java.net.URI + +data class ProgrammeResources( + val timetableUri: URI, + val evaluationsUri: URI +) diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/pdfextractor/EvaluationsExtractor.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/pdfextractor/EvaluationsExtractor.kt new file mode 100644 index 00000000..efc808ab --- /dev/null +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/pdfextractor/EvaluationsExtractor.kt @@ -0,0 +1,17 @@ +package org.ionproject.integration.infrastructure.pdfextractor + +import org.ionproject.integration.infrastructure.Try + +object EvaluationsExtractor { + // Arguments to pass to tabula + // -g = Guess the portion of the page to analyze per page + // -t = Force PDF to be extracted using stream-mode + // -p = Page range + // -f = Output format JSON + // -a top, left, bottom, right + + val evaluationsTable = object : IPdfExtractor { + override fun extract(pdfPath: String): Try> = + PdfUtils.processPdf(pdfPath, "-g", "-l", "-p", "all", "-f", "JSON") + } +} diff --git a/src/main/kotlin/org/ionproject/integration/ui/dto/CreateJobDto.kt b/src/main/kotlin/org/ionproject/integration/ui/dto/CreateJobDto.kt index c1163433..bffb89b3 100644 --- a/src/main/kotlin/org/ionproject/integration/ui/dto/CreateJobDto.kt +++ b/src/main/kotlin/org/ionproject/integration/ui/dto/CreateJobDto.kt @@ -1,7 +1,6 @@ package org.ionproject.integration.ui.dto import org.ionproject.integration.application.job.JobType -import java.lang.UnsupportedOperationException data class CreateJobDto( val institution: String? = null, @@ -13,7 +12,7 @@ data class CreateJobDto( return when (type) { JobType.TIMETABLE -> SafeTimetableJobDto(institution!!, programme!!, format!!) JobType.ACADEMIC_CALENDAR -> SafeCalendarJobDto(institution!!, format!!) - JobType.EXAM_SCHEDULE -> throw UnsupportedOperationException("Exam Schedule not yet supported") + JobType.EXAM_SCHEDULE -> SafeEvaluationsJobDto(institution!!, programme!!, format!!) } } } diff --git a/src/main/kotlin/org/ionproject/integration/ui/dto/SafeEvaluationsJobDto.kt b/src/main/kotlin/org/ionproject/integration/ui/dto/SafeEvaluationsJobDto.kt new file mode 100644 index 00000000..e199ebcd --- /dev/null +++ b/src/main/kotlin/org/ionproject/integration/ui/dto/SafeEvaluationsJobDto.kt @@ -0,0 +1,23 @@ +package org.ionproject.integration.ui.dto + +import org.ionproject.integration.application.JobEngine +import org.ionproject.integration.infrastructure.file.OutputFormat +import org.ionproject.integration.infrastructure.repository.IInstitutionRepository +import org.ionproject.integration.infrastructure.repository.IProgrammeRepository + +data class SafeEvaluationsJobDto( + override val institution: String, + val programme: String, + override val format: String, +) : SafeJobDto { + override fun toJobRequest( + institutionRepo: IInstitutionRepository, + programmeRepo: IProgrammeRepository + ): JobEngine.AbstractJobRequest { + val format = OutputFormat.of(format) + val institution = institutionRepo.getInstitutionByIdentifier(institution) + val programme = programmeRepo.getProgrammeByAcronymAndInstitution(programme, institution) + + return JobEngine.EvaluationsJobRequest(format, institution, programme) + } +} diff --git a/src/test/kotlin/org/ionproject/integration/dispatcher/FileWriterTests.kt b/src/test/kotlin/org/ionproject/integration/dispatcher/FileWriterTests.kt index 43b4b404..48449ab8 100644 --- a/src/test/kotlin/org/ionproject/integration/dispatcher/FileWriterTests.kt +++ b/src/test/kotlin/org/ionproject/integration/dispatcher/FileWriterTests.kt @@ -10,7 +10,7 @@ import org.ionproject.integration.domain.timetable.dto.ClassDto import org.ionproject.integration.domain.timetable.dto.EventDto import org.ionproject.integration.domain.timetable.dto.InstructorDto import org.ionproject.integration.domain.timetable.dto.ProgrammeDto -import org.ionproject.integration.domain.timetable.dto.SchoolDto +import org.ionproject.integration.domain.common.dto.SchoolDto import org.ionproject.integration.domain.timetable.dto.SectionDto import org.ionproject.integration.domain.timetable.dto.TimetableDto import org.ionproject.integration.infrastructure.file.OutputFormat diff --git a/src/test/kotlin/org/ionproject/integration/format/implementations/TimetableDtoFormatCheckerTest.kt b/src/test/kotlin/org/ionproject/integration/format/implementations/TimetableDtoFormatCheckerTest.kt index 0dbc7360..fc844d70 100644 --- a/src/test/kotlin/org/ionproject/integration/format/implementations/TimetableDtoFormatCheckerTest.kt +++ b/src/test/kotlin/org/ionproject/integration/format/implementations/TimetableDtoFormatCheckerTest.kt @@ -15,7 +15,7 @@ import org.ionproject.integration.domain.timetable.model.Programme import org.ionproject.integration.domain.timetable.dto.ProgrammeDto import org.ionproject.integration.domain.timetable.model.RecurrentEvent import org.ionproject.integration.domain.common.School -import org.ionproject.integration.domain.timetable.dto.SchoolDto +import org.ionproject.integration.domain.common.dto.SchoolDto import org.ionproject.integration.domain.timetable.dto.SectionDto import org.ionproject.integration.domain.timetable.Timetable import org.ionproject.integration.domain.timetable.dto.TimetableDto diff --git a/src/test/kotlin/org/ionproject/integration/step/tasklet/iseltimetable/implementations/WriteFileTaskletTests.kt b/src/test/kotlin/org/ionproject/integration/step/tasklet/iseltimetable/implementations/WriteFileTaskletTests.kt index 1de427ae..d7157c70 100644 --- a/src/test/kotlin/org/ionproject/integration/step/tasklet/iseltimetable/implementations/WriteFileTaskletTests.kt +++ b/src/test/kotlin/org/ionproject/integration/step/tasklet/iseltimetable/implementations/WriteFileTaskletTests.kt @@ -7,7 +7,7 @@ import org.ionproject.integration.application.job.ISELTimetableJob import org.ionproject.integration.domain.timetable.model.Programme import org.ionproject.integration.domain.timetable.dto.ProgrammeDto import org.ionproject.integration.domain.common.School -import org.ionproject.integration.domain.timetable.dto.SchoolDto +import org.ionproject.integration.domain.common.dto.SchoolDto import org.ionproject.integration.domain.timetable.Timetable import org.ionproject.integration.domain.timetable.dto.TimetableDto import org.ionproject.integration.domain.timetable.TimetableTeachers From 62e70ae709442f7360c66057c3b2379fccbe67ef Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Sun, 27 Jun 2021 17:40:17 +0100 Subject: [PATCH 52/67] Business Object Creation --- .../application/job/ISELEvaluationsJob.kt | 2 +- .../domain/evaluations/BusinessObjects.kt | 23 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt b/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt index 11cce47a..df5275ff 100644 --- a/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt +++ b/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt @@ -47,7 +47,7 @@ class ISELEvaluationsJob( .start(taskletStep("Download And Compare", downloadEvaluationsPDFTasklet())) .on("STOPPED").end() .next(extractEvaluationsPDFTasklet()) - // .next(createEvaluationsPDFBusinessObjectsTasklet()) + .next(createEvaluationsPDFBusinessObjectsTasklet()) // .next(createEvaluationsPDFDtoTasklet()) // .next(writeEvaluationsDTOToGitTasklet()) .build().listener(NotificationListener()) diff --git a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt index d8816277..bde5ccca 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt @@ -1,7 +1,11 @@ package org.ionproject.integration.domain.evaluations +import com.squareup.moshi.Types import org.ionproject.integration.domain.common.School import org.ionproject.integration.infrastructure.DateUtils +import org.ionproject.integration.infrastructure.Try +import org.ionproject.integration.infrastructure.pdfextractor.tabula.Table +import org.ionproject.integration.infrastructure.text.JsonUtils import java.time.LocalDateTime import java.time.ZonedDateTime @@ -13,8 +17,9 @@ data class Evaluations( val exams: List ) { companion object { - fun from(rawEvaluationsData: RawEvaluationsData): Evaluations = - Evaluations( + fun from(rawEvaluationsData: RawEvaluationsData): Evaluations { + + return Evaluations( creationDateTime = rawEvaluationsData.creationDate, retrievalDateTime = DateUtils.formatToISO8601(ZonedDateTime.now()), School( @@ -24,6 +29,20 @@ data class Evaluations( calendarTerm = buildCalendarTerm(rawEvaluationsData), emptyList() ) + } + + private fun rawDataToBusiness(rawEvaluationsData: RawEvaluationsData) { + fun String.toTableList(): Try> = + JsonUtils.fromJson(this, Types.newParameterizedType(List::class.java, Table::class.java)) + + rawEvaluationsData.table.toTableList().map { mapTablesToBusiness(rawEvaluationsData, it) } + } + + private fun mapTablesToBusiness( + rawEvaluationsData: RawEvaluationsData, + tableList: List + ) { + } // TODO private fun buildCalendarTerm(rawEvaluationsData: RawEvaluationsData): String = "" From 2c249f30a518f5ff995fe42ded8206f611e7fb09 Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Sun, 27 Jun 2021 19:08:06 +0100 Subject: [PATCH 53/67] Correction to Business Object generation for Academic Calendar generating the School BO through job parameters. --- .../application/job/ISELAcademicCalendarJob.kt | 13 +++++++++++-- .../integration/domain/calendar/BusinessObjects.kt | 7 ++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt b/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt index 9fa5b8ce..6524fbba 100644 --- a/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt +++ b/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt @@ -8,6 +8,7 @@ import org.ionproject.integration.application.dto.AcademicCalendarData import org.ionproject.integration.application.job.tasklet.DownloadAndCompareTasklet import org.ionproject.integration.domain.calendar.AcademicCalendarDto import org.ionproject.integration.domain.calendar.RawCalendarData +import org.ionproject.integration.domain.common.InstitutionModel import org.ionproject.integration.infrastructure.Try import org.ionproject.integration.infrastructure.file.FileComparatorImpl import org.ionproject.integration.infrastructure.file.FileDigestImpl @@ -18,12 +19,14 @@ import org.ionproject.integration.infrastructure.orThrow import org.ionproject.integration.infrastructure.pdfextractor.AcademicCalendarExtractor import org.ionproject.integration.infrastructure.pdfextractor.ITextPdfExtractor import org.ionproject.integration.infrastructure.pdfextractor.PDFBytesFormatChecker +import org.ionproject.integration.infrastructure.repository.IInstitutionRepository import org.ionproject.integration.model.external.calendar.AcademicCalendar import org.ionproject.integration.model.external.calendar.AcademicCalendarDto import org.springframework.batch.core.ExitStatus import org.springframework.batch.core.configuration.annotation.JobBuilderFactory import org.springframework.batch.core.configuration.annotation.StepBuilderFactory import org.springframework.batch.core.configuration.annotation.StepScope +import org.springframework.batch.core.scope.context.ChunkContext import org.springframework.batch.core.step.tasklet.Tasklet import org.springframework.batch.core.step.tasklet.TaskletStep import org.springframework.batch.repeat.RepeatStatus @@ -43,6 +46,7 @@ class ISELAcademicCalendarJob( val properties: AppProperties, val downloader: IFileDownloader, val dispatcher: IDispatcher, + private val institutionRepository: IInstitutionRepository, @Autowired val ds: DataSource ) { @@ -117,12 +121,17 @@ class ISELAcademicCalendarJob( @Bean fun createCalendarPDFBusinessObjectsTasklet() = stepBuilderFactory.get("Create Business Objects from Calendar Raw Data") - .tasklet { _, _ -> - State.academicCalendar = AcademicCalendar.from(State.rawCalendarData) + .tasklet { _, context -> + State.academicCalendar = AcademicCalendar.from(State.rawCalendarData, getJobInstitution(context)) RepeatStatus.FINISHED } .build() + private fun getJobInstitution(context: ChunkContext): InstitutionModel = + institutionRepository.getInstitutionByIdentifier( + context.stepContext.jobParameters[JobEngine.INSTITUTION_PARAMETER] as String + ) + @Bean fun createCalendarPDFDtoTasklet() = stepBuilderFactory.get("Create DTO from Calendar Business Objects") .tasklet { _, _ -> diff --git a/src/main/kotlin/org/ionproject/integration/domain/calendar/BusinessObjects.kt b/src/main/kotlin/org/ionproject/integration/domain/calendar/BusinessObjects.kt index 9786cc56..ce9454f9 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/calendar/BusinessObjects.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/calendar/BusinessObjects.kt @@ -3,6 +3,7 @@ package org.ionproject.integration.model.external.calendar import org.ionproject.integration.domain.common.Language import org.ionproject.integration.domain.common.School import org.ionproject.integration.domain.calendar.RawCalendarData +import org.ionproject.integration.domain.common.InstitutionModel import org.ionproject.integration.infrastructure.DateUtils import org.ionproject.integration.infrastructure.text.RegexUtils import java.time.LocalDate @@ -23,13 +24,13 @@ data class AcademicCalendar( private const val PT_EVALUATION_REGEX = "\\b(?:Exames|Testes)\\b" private const val PT_LECTURES_REGEX = "\\b(?:Turmas)\\b" - fun from(rawCalendarData: RawCalendarData): AcademicCalendar = + fun from(rawCalendarData: RawCalendarData, jobInstitution: InstitutionModel): AcademicCalendar = AcademicCalendar( creationDateTime = rawCalendarData.creationDate, retrievalDateTime = DateUtils.formatToISO8601(ZonedDateTime.now()), School( - "Instituto Superior de Engenharia de Lisboa", - "ISEL" + jobInstitution.name, + jobInstitution.acronym ), Language.PT, buildTerms(rawCalendarData) From 1fe0de4652839d65cb0fd7477c54271544908455 Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Sat, 3 Jul 2021 15:04:16 +0100 Subject: [PATCH 54/67] First version of the Evaluations Business Objects tests. The creation date of a pdf is now retrieved from file properties and not from PDF properties since some don't have it. --- .../job/ISELAcademicCalendarJob.kt | 2 +- .../application/job/ISELEvaluationsJob.kt | 16 +++- .../domain/evaluations/BusinessObjects.kt | 9 ++- .../pdfextractor/ITextPdfExtractor.kt | 22 +++--- ...EvaluationsBusinessObjFormatCheckerTest.kt | 72 ++++++++++++++++++ src/test/resources/evaluationsTest.pdf | Bin 0 -> 42795 bytes 6 files changed, 102 insertions(+), 19 deletions(-) create mode 100644 src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt create mode 100644 src/test/resources/evaluationsTest.pdf diff --git a/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt b/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt index 6524fbba..128a575d 100644 --- a/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt +++ b/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt @@ -46,7 +46,7 @@ class ISELAcademicCalendarJob( val properties: AppProperties, val downloader: IFileDownloader, val dispatcher: IDispatcher, - private val institutionRepository: IInstitutionRepository, + val institutionRepository: IInstitutionRepository, @Autowired val ds: DataSource ) { diff --git a/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt b/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt index df5275ff..a1c8c432 100644 --- a/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt +++ b/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt @@ -4,6 +4,7 @@ import org.ionproject.integration.application.JobEngine import org.ionproject.integration.application.config.AppProperties import org.ionproject.integration.application.dispatcher.IDispatcher import org.ionproject.integration.application.job.tasklet.DownloadAndCompareTasklet +import org.ionproject.integration.domain.common.InstitutionModel import org.ionproject.integration.domain.evaluations.Evaluations import org.ionproject.integration.domain.evaluations.EvaluationsDto import org.ionproject.integration.domain.evaluations.RawEvaluationsData @@ -16,9 +17,11 @@ import org.ionproject.integration.infrastructure.orThrow import org.ionproject.integration.infrastructure.pdfextractor.EvaluationsExtractor import org.ionproject.integration.infrastructure.pdfextractor.ITextPdfExtractor import org.ionproject.integration.infrastructure.pdfextractor.PDFBytesFormatChecker +import org.ionproject.integration.infrastructure.repository.IInstitutionRepository import org.springframework.batch.core.configuration.annotation.JobBuilderFactory import org.springframework.batch.core.configuration.annotation.StepBuilderFactory import org.springframework.batch.core.configuration.annotation.StepScope +import org.springframework.batch.core.scope.context.ChunkContext import org.springframework.batch.core.step.tasklet.Tasklet import org.springframework.batch.core.step.tasklet.TaskletStep import org.springframework.batch.repeat.RepeatStatus @@ -38,6 +41,7 @@ class ISELEvaluationsJob( val properties: AppProperties, val downloader: IFileDownloader, val dispatcher: IDispatcher, + val institutionRepository: IInstitutionRepository, @Autowired val ds: DataSource ) { @@ -79,9 +83,8 @@ class ISELEvaluationsJob( @Bean fun extractEvaluationsPDFTasklet() = stepBuilderFactory.get("Extract Evaluations PDF Raw Data") - .tasklet { stepContribution, context -> + .tasklet { stepContribution, _ -> val path = stepContribution.stepExecution.jobExecution.executionContext.get("file-path").toString() - val school = context.stepContext.jobParameters[JobEngine.INSTITUTION_PARAMETER] as String State.rawEvaluationsData = extractEvaluationsPDF(path) RepeatStatus.FINISHED } @@ -112,12 +115,17 @@ class ISELEvaluationsJob( @Bean fun createEvaluationsPDFBusinessObjectsTasklet() = stepBuilderFactory.get("Create Business Objects from Evaluations Raw Data") - .tasklet { _, _ -> - State.evaluations = Evaluations.from(State.rawEvaluationsData) + .tasklet { _, context -> + State.evaluations = Evaluations.from(State.rawEvaluationsData, getJobInstitution(context)) RepeatStatus.FINISHED } .build() + private fun getJobInstitution(context: ChunkContext): InstitutionModel = + institutionRepository.getInstitutionByIdentifier( + context.stepContext.jobParameters[JobEngine.INSTITUTION_PARAMETER] as String + ) + @Bean fun createEvaluationsPDFDtoTasklet() = stepBuilderFactory.get("Create DTO from Evaluations Business Objects") .tasklet { _, _ -> diff --git a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt index bde5ccca..b6953cf5 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt @@ -1,6 +1,7 @@ package org.ionproject.integration.domain.evaluations import com.squareup.moshi.Types +import org.ionproject.integration.domain.common.InstitutionModel import org.ionproject.integration.domain.common.School import org.ionproject.integration.infrastructure.DateUtils import org.ionproject.integration.infrastructure.Try @@ -17,14 +18,14 @@ data class Evaluations( val exams: List ) { companion object { - fun from(rawEvaluationsData: RawEvaluationsData): Evaluations { + fun from(rawEvaluationsData: RawEvaluationsData, jobInstitution: InstitutionModel): Evaluations { return Evaluations( creationDateTime = rawEvaluationsData.creationDate, retrievalDateTime = DateUtils.formatToISO8601(ZonedDateTime.now()), School( - "Instituto Superior de Engenharia de Lisboa", - "ISEL" + jobInstitution.name, + jobInstitution.acronym ), calendarTerm = buildCalendarTerm(rawEvaluationsData), emptyList() @@ -45,7 +46,7 @@ data class Evaluations( } // TODO - private fun buildCalendarTerm(rawEvaluationsData: RawEvaluationsData): String = "" + private fun buildCalendarTerm(rawEvaluationsData: RawEvaluationsData): String = "2020-2021-2" } } diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/pdfextractor/ITextPdfExtractor.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/pdfextractor/ITextPdfExtractor.kt index 71d77a13..0bd9722e 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/pdfextractor/ITextPdfExtractor.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/pdfextractor/ITextPdfExtractor.kt @@ -1,16 +1,17 @@ package org.ionproject.integration.infrastructure.pdfextractor -import com.itextpdf.kernel.pdf.PdfDate import com.itextpdf.kernel.pdf.PdfDocument -import com.itextpdf.kernel.pdf.PdfName import com.itextpdf.kernel.pdf.PdfReader import com.itextpdf.kernel.pdf.canvas.parser.PdfTextExtractor -import org.ionproject.integration.infrastructure.exception.PdfExtractorException import org.ionproject.integration.infrastructure.DateUtils import org.ionproject.integration.infrastructure.Try +import org.ionproject.integration.infrastructure.exception.PdfExtractorException +import java.nio.file.LinkOption +import java.nio.file.Path +import java.nio.file.attribute.BasicFileAttributes import java.time.ZoneId import java.time.ZonedDateTime -import java.util.Calendar +import kotlin.io.path.readAttributes class ITextPdfExtractor : IPdfExtractor { /** @@ -32,7 +33,7 @@ class ITextPdfExtractor : IPdfExtractor { val pageData = PdfTextExtractor.getTextFromPage(pdfDoc.getPage(i)) data.add(pageData) } - data.add(getCreationDateFromPdfDocument(pdfDoc)) + data.add(getCreationDateFromPdfDocument(pdfPath)) } .map { data.toList() } .mapError { PdfExtractorException("Itext cannot process file") } @@ -42,13 +43,14 @@ class ITextPdfExtractor : IPdfExtractor { } } - private fun getCreationDateFromPdfDocument(pdfDocument: PdfDocument): String { - val creationDateString = pdfDocument.documentInfo.getMoreInfo(PdfName.CreationDate.value) - val creationDateCalendar = PdfDate.decode(creationDateString) ?: Calendar.getInstance() - + private fun getCreationDateFromPdfDocument(pdfPath: String): String { + val creationDate = + Path.of(pdfPath) + .readAttributes(LinkOption.NOFOLLOW_LINKS) + .creationTime() return DateUtils.formatToISO8601( ZonedDateTime.ofInstant( - creationDateCalendar.toInstant(), + creationDate.toInstant(), ZoneId.systemDefault() ) ) diff --git a/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt b/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt new file mode 100644 index 00000000..5544a3f2 --- /dev/null +++ b/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt @@ -0,0 +1,72 @@ +package org.ionproject.integration.format.implementations + +import org.ionproject.integration.application.config.AppProperties +import org.ionproject.integration.application.dispatcher.IDispatcher +import org.ionproject.integration.application.job.ISELEvaluationsJob +import org.ionproject.integration.domain.common.InstitutionModel +import org.ionproject.integration.domain.common.School +import org.ionproject.integration.domain.evaluations.Evaluations +import org.ionproject.integration.infrastructure.http.IFileDownloader +import org.ionproject.integration.infrastructure.repository.IInstitutionRepository +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.mockito.kotlin.mock +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory +import java.io.File +import java.net.URI +import javax.sql.DataSource + +class EvaluationsBusinessObjFormatCheckerTest { + + private val mockJobBuilderFactory = mock {} + + private val mockStepBuilderFactory = mock {} + + private val mockAppProperties = mock {} + + private val mockDownloader = mock {} + + private val mockDispatcher = mock {} + + private val mockInstitutionRepository = mock {} + + private val mockDataSource = mock {} + + @Test + fun `when given an evaluations pdf if business object extraction is as expected then success`() { + val resourceFile = File("src/test/resources/evaluationsTest.pdf") + + val job = ISELEvaluationsJob( + mockJobBuilderFactory, + mockStepBuilderFactory, + mockAppProperties, + mockDownloader, + mockDispatcher, + mockInstitutionRepository, + mockDataSource + ) + val evaluationsData = job.extractEvaluationsPDF(resourceFile.toPath().toString()) + val institution = InstitutionModel( + "Instituto Superior de Engenharia de Lisboa", + "ISEL", + "pt.ipl.isel", + URI("") + ) + val academicCalendarRetrieved = Evaluations.from(evaluationsData, institution) + + val academicCalendarExpected = + Evaluations( + academicCalendarRetrieved.creationDateTime, // 2020-2021 Evaluations PDF doesn't have a creation date in it's properties, so it gets the retrieval date time. + academicCalendarRetrieved.retrievalDateTime, + School( + "Instituto Superior de Engenharia de Lisboa", + "ISEL" + ), + "2020-2021-2", + emptyList() + ) + + assertEquals(academicCalendarExpected, academicCalendarRetrieved) + } +} diff --git a/src/test/resources/evaluationsTest.pdf b/src/test/resources/evaluationsTest.pdf new file mode 100644 index 0000000000000000000000000000000000000000..782987fc997cf5d2557fcb9fe787b54d7217a164 GIT binary patch literal 42795 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 literal 0 HcmV?d00001 From d9105734d6dab2a3fe36962d235f70db68f8733e Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Sat, 3 Jul 2021 22:52:46 +0100 Subject: [PATCH 55/67] Changed Programme to Model.Common package. --- .../{timetable/model => common}/Programme.kt | 2 +- .../timetable/IselTimetableTeachersBuilder.kt | 22 +++++++++---------- .../integration/domain/timetable/Timetable.kt | 2 +- .../domain/timetable/model/CourseTeacher.kt | 1 + .../pdfextractor/tabula/Table.kt | 18 +++++++++------ .../TimetableDtoFormatCheckerTest.kt | 2 +- .../implementations/WriteFileTaskletTests.kt | 2 +- 7 files changed, 27 insertions(+), 22 deletions(-) rename src/main/kotlin/org/ionproject/integration/domain/{timetable/model => common}/Programme.kt (57%) diff --git a/src/main/kotlin/org/ionproject/integration/domain/timetable/model/Programme.kt b/src/main/kotlin/org/ionproject/integration/domain/common/Programme.kt similarity index 57% rename from src/main/kotlin/org/ionproject/integration/domain/timetable/model/Programme.kt rename to src/main/kotlin/org/ionproject/integration/domain/common/Programme.kt index c33fe3d7..fed6be98 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/timetable/model/Programme.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/common/Programme.kt @@ -1,4 +1,4 @@ -package org.ionproject.integration.domain.timetable.model +package org.ionproject.integration.domain.common data class Programme( var name: String = "", diff --git a/src/main/kotlin/org/ionproject/integration/domain/timetable/IselTimetableTeachersBuilder.kt b/src/main/kotlin/org/ionproject/integration/domain/timetable/IselTimetableTeachersBuilder.kt index bc86badc..057ce711 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/timetable/IselTimetableTeachersBuilder.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/timetable/IselTimetableTeachersBuilder.kt @@ -1,30 +1,30 @@ package org.ionproject.integration.domain.timetable import com.squareup.moshi.Types -import org.ionproject.integration.domain.exception.TimetableTeachersBuilderException -import org.ionproject.integration.domain.timetable.model.ClassDetail -import org.ionproject.integration.domain.timetable.model.Course -import org.ionproject.integration.domain.timetable.model.CourseTeacher -import org.ionproject.integration.domain.timetable.model.Faculty -import org.ionproject.integration.domain.timetable.model.Label import org.ionproject.integration.domain.common.Language -import org.ionproject.integration.domain.timetable.model.Programme -import org.ionproject.integration.domain.timetable.model.RecurrentEvent +import org.ionproject.integration.domain.common.Programme import org.ionproject.integration.domain.common.School import org.ionproject.integration.domain.common.Weekday +import org.ionproject.integration.domain.exception.TimetableTeachersBuilderException import org.ionproject.integration.domain.timetable.dto.FacultyDTO import org.ionproject.integration.domain.timetable.dto.FacultyRawData import org.ionproject.integration.domain.timetable.dto.RawTimetableData import org.ionproject.integration.domain.timetable.dto.toDto +import org.ionproject.integration.domain.timetable.model.ClassDetail +import org.ionproject.integration.domain.timetable.model.Course +import org.ionproject.integration.domain.timetable.model.CourseTeacher +import org.ionproject.integration.domain.timetable.model.Faculty +import org.ionproject.integration.domain.timetable.model.Label +import org.ionproject.integration.domain.timetable.model.RecurrentEvent +import org.ionproject.integration.infrastructure.DateUtils +import org.ionproject.integration.infrastructure.Try +import org.ionproject.integration.infrastructure.orThrow import org.ionproject.integration.infrastructure.pdfextractor.tabula.Cell import org.ionproject.integration.infrastructure.pdfextractor.tabula.Table -import org.ionproject.integration.infrastructure.DateUtils import org.ionproject.integration.infrastructure.text.IgnoredWords import org.ionproject.integration.infrastructure.text.JsonUtils import org.ionproject.integration.infrastructure.text.RegexUtils -import org.ionproject.integration.infrastructure.Try import org.ionproject.integration.infrastructure.text.generateAcronym -import org.ionproject.integration.infrastructure.orThrow import java.time.Duration import java.time.LocalTime import java.time.ZonedDateTime diff --git a/src/main/kotlin/org/ionproject/integration/domain/timetable/Timetable.kt b/src/main/kotlin/org/ionproject/integration/domain/timetable/Timetable.kt index 9a9994ba..c53c0bb3 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/timetable/Timetable.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/timetable/Timetable.kt @@ -1,7 +1,7 @@ package org.ionproject.integration.domain.timetable import org.ionproject.integration.domain.timetable.model.Course -import org.ionproject.integration.domain.timetable.model.Programme +import org.ionproject.integration.domain.common.Programme import org.ionproject.integration.domain.common.School data class Timetable( diff --git a/src/main/kotlin/org/ionproject/integration/domain/timetable/model/CourseTeacher.kt b/src/main/kotlin/org/ionproject/integration/domain/timetable/model/CourseTeacher.kt index 05d62a23..72463632 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/timetable/model/CourseTeacher.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/timetable/model/CourseTeacher.kt @@ -1,5 +1,6 @@ package org.ionproject.integration.domain.timetable.model +import org.ionproject.integration.domain.common.Programme import org.ionproject.integration.domain.common.School data class CourseTeacher( diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/pdfextractor/tabula/Table.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/pdfextractor/tabula/Table.kt index bdec57f7..28a91c00 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/pdfextractor/tabula/Table.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/pdfextractor/tabula/Table.kt @@ -10,6 +10,10 @@ data class Table( val bottom: Double, val data: Array> ) { + companion object { + private const val HASH_PRIME_NUMBER = 31 + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -30,13 +34,13 @@ data class Table( override fun hashCode(): Int { var result = extraction_method.hashCode() - result = 31 * result + top.hashCode() - result = 31 * result + left.hashCode() - result = 31 * result + width.hashCode() - result = 31 * result + height.hashCode() - result = 31 * result + right.hashCode() - result = 31 * result + bottom.hashCode() - result = 31 * result + data.contentDeepHashCode() + result = HASH_PRIME_NUMBER * result + top.hashCode() + result = HASH_PRIME_NUMBER * result + left.hashCode() + result = HASH_PRIME_NUMBER * result + width.hashCode() + result = HASH_PRIME_NUMBER * result + height.hashCode() + result = HASH_PRIME_NUMBER * result + right.hashCode() + result = HASH_PRIME_NUMBER * result + bottom.hashCode() + result = HASH_PRIME_NUMBER * result + data.contentDeepHashCode() return result } } diff --git a/src/test/kotlin/org/ionproject/integration/format/implementations/TimetableDtoFormatCheckerTest.kt b/src/test/kotlin/org/ionproject/integration/format/implementations/TimetableDtoFormatCheckerTest.kt index fc844d70..90bc5527 100644 --- a/src/test/kotlin/org/ionproject/integration/format/implementations/TimetableDtoFormatCheckerTest.kt +++ b/src/test/kotlin/org/ionproject/integration/format/implementations/TimetableDtoFormatCheckerTest.kt @@ -11,7 +11,7 @@ import org.ionproject.integration.domain.timetable.model.Faculty import org.ionproject.integration.domain.timetable.model.Instructor import org.ionproject.integration.domain.timetable.dto.InstructorDto import org.ionproject.integration.domain.timetable.model.Label -import org.ionproject.integration.domain.timetable.model.Programme +import org.ionproject.integration.domain.common.Programme import org.ionproject.integration.domain.timetable.dto.ProgrammeDto import org.ionproject.integration.domain.timetable.model.RecurrentEvent import org.ionproject.integration.domain.common.School diff --git a/src/test/kotlin/org/ionproject/integration/step/tasklet/iseltimetable/implementations/WriteFileTaskletTests.kt b/src/test/kotlin/org/ionproject/integration/step/tasklet/iseltimetable/implementations/WriteFileTaskletTests.kt index d7157c70..57811f2a 100644 --- a/src/test/kotlin/org/ionproject/integration/step/tasklet/iseltimetable/implementations/WriteFileTaskletTests.kt +++ b/src/test/kotlin/org/ionproject/integration/step/tasklet/iseltimetable/implementations/WriteFileTaskletTests.kt @@ -4,7 +4,7 @@ import org.ionproject.integration.application.job.tasklet.WriteFileTasklet import org.ionproject.integration.application.dispatcher.DispatchResult import org.ionproject.integration.application.dispatcher.IDispatcher import org.ionproject.integration.application.job.ISELTimetableJob -import org.ionproject.integration.domain.timetable.model.Programme +import org.ionproject.integration.domain.common.Programme import org.ionproject.integration.domain.timetable.dto.ProgrammeDto import org.ionproject.integration.domain.common.School import org.ionproject.integration.domain.common.dto.SchoolDto From 5a87ea853d37249efc34559f604f29b9bc884e85 Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Tue, 6 Jul 2021 00:42:05 +0100 Subject: [PATCH 56/67] Parser for Evaluation Date and Time done in DateUtils with getEvaluationDateTimeFrom(..). Tests added in DateUtilsTests.kt --- .../integration/infrastructure/DateUtils.kt | 28 +++++++++++++++++ .../integration/utils/DateUtilsTests.kt | 30 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/DateUtils.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/DateUtils.kt index aa27553d..a3ba0029 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/DateUtils.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/DateUtils.kt @@ -2,6 +2,7 @@ package org.ionproject.integration.infrastructure import java.time.LocalDate import java.time.LocalDateTime +import java.time.LocalTime import java.time.Month import java.time.ZoneId import java.time.ZonedDateTime @@ -91,6 +92,28 @@ object DateUtils { return IntervalDate(fromDate, toDate) } + fun getEvaluationDateTimeFrom( + yearStr: String, + dayMonthStr: String, + timeStr: String, + durationStr: String + ): IntervalDateTime { + val timeFormat = DateTimeFormatter.ofPattern("HH'h'mm") + val durationFormat = DateTimeFormatter.ofPattern("H'h'mm") + val dateFormat = DateTimeFormatter.ofPattern("d MMM yyyy", localePT) + + val time = LocalTime.parse(timeStr, timeFormat) + val duration = LocalTime.parse(durationStr, durationFormat) + val date = LocalDate.parse(dayMonthStr.split(".")[0] + " " + yearStr, dateFormat) + + val startDateTime = LocalDateTime.of(date, time) + val endDateTime = LocalDateTime.of(date, addToStartTime(time, duration)) + return IntervalDateTime(startDateTime, endDateTime) + } + + private fun addToStartTime(time: LocalTime, duration: LocalTime): LocalTime = + time.plusHours(duration.hour.toLong()).plusMinutes(duration.minute.toLong()) + private fun buildBeginDate(string: String, month: Month, year: Int): LocalDate { if (isMonthAndYearUnavailable(string)) return LocalDate.of(year, month, string.toInt()) @@ -104,6 +127,11 @@ object DateUtils { private fun isMonthAndYearUnavailable(string: String): Boolean = string.length <= 2 } +data class IntervalDateTime( + val from: LocalDateTime, + val to: LocalDateTime +) + data class IntervalDate( val from: LocalDate, val to: LocalDate diff --git a/src/test/kotlin/org/ionproject/integration/utils/DateUtilsTests.kt b/src/test/kotlin/org/ionproject/integration/utils/DateUtilsTests.kt index 17af2f80..555ac414 100644 --- a/src/test/kotlin/org/ionproject/integration/utils/DateUtilsTests.kt +++ b/src/test/kotlin/org/ionproject/integration/utils/DateUtilsTests.kt @@ -158,4 +158,34 @@ class DateUtilsTests { assertEquals("1982-05-11", DateUtils.formatToCalendarDate(intervalDate.from)) assertEquals("1982-05-17", DateUtils.formatToCalendarDate(intervalDate.to)) } + + @Test + fun `when Date Time Range and Duration are presented then Return a IntervalDateTime`() { + // Arrange + val dayMonthStr = "29 jun. (terça)" + val timeStr = "19h00" + val durationStr = "3h00" + + // Act + val intervalDate = DateUtils.getEvaluationDateTimeFrom("2021", dayMonthStr, timeStr, durationStr) + + // Assert + assertEquals("2021-06-29T19:00", intervalDate.from.toString()) + assertEquals("2021-06-29T22:00", intervalDate.to.toString()) + } + + @Test + fun `when Date Time Range with and Duration with Half-Hour are presented then Return a IntervalDateTime`() { + // Arrange + val dayMonthStr = "2 jun. (segunda)" + val timeStr = "19h30" + val durationStr = "2h30" + + // Act + val intervalDate = DateUtils.getEvaluationDateTimeFrom("2021", dayMonthStr, timeStr, durationStr) + + // Assert + assertEquals("2021-06-02T19:30", intervalDate.from.toString()) + assertEquals("2021-06-02T22:00", intervalDate.to.toString()) + } } From 378e4e7da643252dcc1ccf21a263d5d4e7f9d34c Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Tue, 6 Jul 2021 13:26:32 +0100 Subject: [PATCH 57/67] First version of exam list builder with unitary tests. --- .../application/job/ISELEvaluationsJob.kt | 20 +- .../domain/evaluations/BusinessObjects.kt | 146 +++- ...EvaluationsBusinessObjFormatCheckerTest.kt | 719 +++++++++++++++++- 3 files changed, 854 insertions(+), 31 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt b/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt index a1c8c432..2c4b0062 100644 --- a/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt +++ b/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt @@ -5,6 +5,7 @@ import org.ionproject.integration.application.config.AppProperties import org.ionproject.integration.application.dispatcher.IDispatcher import org.ionproject.integration.application.job.tasklet.DownloadAndCompareTasklet import org.ionproject.integration.domain.common.InstitutionModel +import org.ionproject.integration.domain.common.ProgrammeModel import org.ionproject.integration.domain.evaluations.Evaluations import org.ionproject.integration.domain.evaluations.EvaluationsDto import org.ionproject.integration.domain.evaluations.RawEvaluationsData @@ -18,6 +19,7 @@ import org.ionproject.integration.infrastructure.pdfextractor.EvaluationsExtract import org.ionproject.integration.infrastructure.pdfextractor.ITextPdfExtractor import org.ionproject.integration.infrastructure.pdfextractor.PDFBytesFormatChecker import org.ionproject.integration.infrastructure.repository.IInstitutionRepository +import org.ionproject.integration.infrastructure.repository.IProgrammeRepository import org.springframework.batch.core.configuration.annotation.JobBuilderFactory import org.springframework.batch.core.configuration.annotation.StepBuilderFactory import org.springframework.batch.core.configuration.annotation.StepScope @@ -42,6 +44,7 @@ class ISELEvaluationsJob( val downloader: IFileDownloader, val dispatcher: IDispatcher, val institutionRepository: IInstitutionRepository, + val programmeRepository: IProgrammeRepository, @Autowired val ds: DataSource ) { @@ -72,15 +75,6 @@ class ISELEvaluationsJob( return DownloadAndCompareTasklet(downloader, pdfChecker, fileComparator) } -/* @Bean - fun downloadEvaluationsPDFTasklet() = stepBuilderFactory.get("Download Calendar PDF") - .tasklet { stepContribution, chunkContext -> - val pdfChecker = PDFBytesFormatChecker() - val fileComparator = FileComparatorImpl(FileDigestImpl(), HashRepositoryImpl(ds)) - DownloadAndCompareTasklet(downloader, pdfChecker, fileComparator).execute(stepContribution, chunkContext) - } - .build()*/ - @Bean fun extractEvaluationsPDFTasklet() = stepBuilderFactory.get("Extract Evaluations PDF Raw Data") .tasklet { stepContribution, _ -> @@ -116,11 +110,17 @@ class ISELEvaluationsJob( fun createEvaluationsPDFBusinessObjectsTasklet() = stepBuilderFactory.get("Create Business Objects from Evaluations Raw Data") .tasklet { _, context -> - State.evaluations = Evaluations.from(State.rawEvaluationsData, getJobInstitution(context)) + State.evaluations = Evaluations.from(State.rawEvaluationsData, getJobProgramme(context)) RepeatStatus.FINISHED } .build() + private fun getJobProgramme(context: ChunkContext): ProgrammeModel = + programmeRepository.getProgrammeByAcronymAndInstitution( + context.stepContext.jobParameters[JobEngine.PROGRAMME_PARAMETER] as String, + getJobInstitution(context) + ) + private fun getJobInstitution(context: ChunkContext): InstitutionModel = institutionRepository.getInstitutionByIdentifier( context.stepContext.jobParameters[JobEngine.INSTITUTION_PARAMETER] as String diff --git a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt index b6953cf5..2253847e 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt @@ -1,10 +1,12 @@ package org.ionproject.integration.domain.evaluations import com.squareup.moshi.Types -import org.ionproject.integration.domain.common.InstitutionModel +import org.ionproject.integration.domain.common.Programme +import org.ionproject.integration.domain.common.ProgrammeModel import org.ionproject.integration.domain.common.School import org.ionproject.integration.infrastructure.DateUtils import org.ionproject.integration.infrastructure.Try +import org.ionproject.integration.infrastructure.orThrow import org.ionproject.integration.infrastructure.pdfextractor.tabula.Table import org.ionproject.integration.infrastructure.text.JsonUtils import java.time.LocalDateTime @@ -13,40 +15,123 @@ import java.time.ZonedDateTime data class Evaluations( val creationDateTime: String = "", val retrievalDateTime: String = "", - val school: School = School(), + val school: School, + val programme: Programme, val calendarTerm: String = "", val exams: List ) { companion object { - fun from(rawEvaluationsData: RawEvaluationsData, jobInstitution: InstitutionModel): Evaluations { + private const val CALENDAR_TERM_REGEX = "(\\sAno\\sLetivo\\s?:\\s?)(.+?(\\r|\\R))" + + fun from(rawEvaluationsData: RawEvaluationsData, jobProgramme: ProgrammeModel): Evaluations { return Evaluations( creationDateTime = rawEvaluationsData.creationDate, retrievalDateTime = DateUtils.formatToISO8601(ZonedDateTime.now()), School( - jobInstitution.name, - jobInstitution.acronym + jobProgramme.institutionModel.name, + jobProgramme.institutionModel.acronym + ), + Programme( + jobProgramme.name, + jobProgramme.acronym ), calendarTerm = buildCalendarTerm(rawEvaluationsData), - emptyList() + buildExamList(rawEvaluationsData, jobProgramme) ) } - private fun rawDataToBusiness(rawEvaluationsData: RawEvaluationsData) { - fun String.toTableList(): Try> = - JsonUtils.fromJson(this, Types.newParameterizedType(List::class.java, Table::class.java)) + // TODO + private fun buildCalendarTerm(rawEvaluationsData: RawEvaluationsData): String = "2020-2021-2" - rawEvaluationsData.table.toTableList().map { mapTablesToBusiness(rawEvaluationsData, it) } - } + private fun buildExamList(rawEvaluationsData: RawEvaluationsData, jobProgramme: ProgrammeModel): List = + rawEvaluationsData.table.toTableList().map { getExamsFromTable(it, jobProgramme.acronym) }.orThrow() - private fun mapTablesToBusiness( - rawEvaluationsData: RawEvaluationsData, - tableList: List
- ) { - } + private fun String.toTableList(): Try> = + JsonUtils.fromJson(this, Types.newParameterizedType(List::class.java, Table::class.java)) - // TODO - private fun buildCalendarTerm(rawEvaluationsData: RawEvaluationsData): String = "2020-2021-2" + private fun getExamsFromTable( + tableList: List
, + programmeAcronym: String + ): List { + val examList = mutableListOf() + for (table in tableList) { + for (line in table.data) { + val cleanedLine = line.dropWhile { it.text.isBlank() } + if (cleanedLine[TableColumn.SUMMER_EXAM_PROGRAMME.ordinal].text.contains(programmeAcronym)) { + val intervalDateTimeNormal = + DateUtils.getEvaluationDateTimeFrom( + "2021", + cleanedLine[TableColumn.NORMAL_EXAM_DATE.ordinal].text, + cleanedLine[TableColumn.NORMAL_EXAM_TIME.ordinal].text, + cleanedLine[TableColumn.NORMAL_EXAM_DURATION.ordinal].text + ) + examList.add( + Exam( + cleanedLine[TableColumn.COURSE.ordinal].text, + intervalDateTimeNormal.from, + intervalDateTimeNormal.to, + ExamCategory.EXAM_NORMAL, + "" + ) + ) + val intervalDateTimeAltern = + DateUtils.getEvaluationDateTimeFrom( + "2021", + cleanedLine[TableColumn.ALTERN_EXAM_DATE.ordinal].text, + cleanedLine[TableColumn.ALTERN_EXAM_TIME.ordinal].text, + cleanedLine[TableColumn.ALTERN_EXAM_DURATION.ordinal].text + ) + examList.add( + Exam( + cleanedLine[TableColumn.COURSE.ordinal].text, + intervalDateTimeAltern.from, + intervalDateTimeAltern.to, + ExamCategory.EXAM_ALTERN, + "" + ) + ) + val intervalDateTimeSpecial = + DateUtils.getEvaluationDateTimeFrom( + "2021", + cleanedLine[TableColumn.SPECIAL_EXAM_DATE.ordinal].text, + cleanedLine[TableColumn.SPECIAL_EXAM_TIME.ordinal].text, + cleanedLine[TableColumn.SPECIAL_EXAM_DURATION.ordinal].text + ) + examList.add( + Exam( + cleanedLine[TableColumn.COURSE.ordinal].text, + intervalDateTimeSpecial.from, + intervalDateTimeSpecial.to, + ExamCategory.EXAM_SPECIAL, + "" + ) + ) + } + if (cleanedLine[TableColumn.WINTER_EXAM_PROGRAMME.ordinal].text.contains(programmeAcronym) && + !cleanedLine[TableColumn.SUMMER_EXAM_PROGRAMME.ordinal].text.contains(programmeAcronym) + ) { + val intervalDateTimeSpecial = + DateUtils.getEvaluationDateTimeFrom( + "2021", + cleanedLine[TableColumnWinterCourse.SPECIAL_EXAM_DATE.ordinal].text, + cleanedLine[TableColumnWinterCourse.SPECIAL_EXAM_TIME.ordinal].text, + cleanedLine[TableColumnWinterCourse.SPECIAL_EXAM_DURATION.ordinal].text + ) + examList.add( + Exam( + cleanedLine[TableColumnWinterCourse.COURSE.ordinal].text, + intervalDateTimeSpecial.from, + intervalDateTimeSpecial.to, + ExamCategory.EXAM_SPECIAL, + "" + ) + ) + } + } + } + return examList.toList() + } } } @@ -64,3 +149,28 @@ enum class ExamCategory { EXAM_ALTERN, EXAM_SPECIAL } + +private enum class TableColumn { + COURSE, + WINTER_EXAM_PROGRAMME, + SUMMER_EXAM_PROGRAMME, + NORMAL_EXAM_DATE, + NORMAL_EXAM_TIME, + NORMAL_EXAM_DURATION, + ALTERN_EXAM_DATE, + ALTERN_EXAM_TIME, + ALTERN_EXAM_DURATION, + SPECIAL_EXAM_DATE, + SPECIAL_EXAM_TIME, + SPECIAL_EXAM_DURATION +} + +private enum class TableColumnWinterCourse { + COURSE, + WINTER_EXAM_PROGRAMME, + SUMMER_EXAM_PROGRAMME, + WINTER_COURSE, + SPECIAL_EXAM_DATE, + SPECIAL_EXAM_TIME, + SPECIAL_EXAM_DURATION +} diff --git a/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt b/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt index 5544a3f2..c6e927d4 100644 --- a/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt +++ b/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt @@ -4,10 +4,16 @@ import org.ionproject.integration.application.config.AppProperties import org.ionproject.integration.application.dispatcher.IDispatcher import org.ionproject.integration.application.job.ISELEvaluationsJob import org.ionproject.integration.domain.common.InstitutionModel +import org.ionproject.integration.domain.common.Programme +import org.ionproject.integration.domain.common.ProgrammeModel +import org.ionproject.integration.domain.common.ProgrammeResources import org.ionproject.integration.domain.common.School import org.ionproject.integration.domain.evaluations.Evaluations +import org.ionproject.integration.domain.evaluations.Exam +import org.ionproject.integration.domain.evaluations.ExamCategory import org.ionproject.integration.infrastructure.http.IFileDownloader import org.ionproject.integration.infrastructure.repository.IInstitutionRepository +import org.ionproject.integration.infrastructure.repository.IProgrammeRepository import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.mockito.kotlin.mock @@ -15,6 +21,7 @@ import org.springframework.batch.core.configuration.annotation.JobBuilderFactory import org.springframework.batch.core.configuration.annotation.StepBuilderFactory import java.io.File import java.net.URI +import java.time.LocalDateTime import javax.sql.DataSource class EvaluationsBusinessObjFormatCheckerTest { @@ -31,6 +38,8 @@ class EvaluationsBusinessObjFormatCheckerTest { private val mockInstitutionRepository = mock {} + private val mockProgrammeRepository = mock {} + private val mockDataSource = mock {} @Test @@ -44,27 +53,731 @@ class EvaluationsBusinessObjFormatCheckerTest { mockDownloader, mockDispatcher, mockInstitutionRepository, + mockProgrammeRepository, mockDataSource ) val evaluationsData = job.extractEvaluationsPDF(resourceFile.toPath().toString()) + val institution = InstitutionModel( "Instituto Superior de Engenharia de Lisboa", "ISEL", "pt.ipl.isel", URI("") ) - val academicCalendarRetrieved = Evaluations.from(evaluationsData, institution) + + val programme = ProgrammeModel( + institution, + "Licenciatura em Engenharia Informática e de Computadores", + "LEIC", + ProgrammeResources( + URI(""), + URI("") + ) + ) + + val academicCalendarRetrieved = Evaluations.from(evaluationsData, programme) val academicCalendarExpected = Evaluations( - academicCalendarRetrieved.creationDateTime, // 2020-2021 Evaluations PDF doesn't have a creation date in it's properties, so it gets the retrieval date time. + academicCalendarRetrieved.creationDateTime, // 2020-2021 Evaluations PDF doesn't have a creation date in its properties, so it gets the retrieval date time. academicCalendarRetrieved.retrievalDateTime, School( "Instituto Superior de Engenharia de Lisboa", "ISEL" ), + Programme( + "Licenciatura em Engenharia Informática e de Computadores", + "LEIC" + ), "2020-2021-2", - emptyList() + listOf( + Exam( + "AApl", + LocalDateTime.of(2021, 7, 7, 10, 0, 0), + LocalDateTime.of(2021, 7, 7, 13, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "AApl", + LocalDateTime.of(2021, 7, 24, 10, 0, 0), + LocalDateTime.of(2021, 7, 24, 13, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "AApl", + LocalDateTime.of(2021, 9, 9, 14, 0, 0), + LocalDateTime.of(2021, 9, 9, 17, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "AC", + LocalDateTime.of(2021, 7, 1, 14, 0, 0), + LocalDateTime.of(2021, 7, 1, 17, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "AC", + LocalDateTime.of(2021, 7, 19, 19, 0, 0), + LocalDateTime.of(2021, 7, 19, 22, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "AC", + LocalDateTime.of(2021, 9, 7, 10, 0, 0), + LocalDateTime.of(2021, 9, 7, 13, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "AED", + LocalDateTime.of(2021, 6, 28, 19, 0, 0), + LocalDateTime.of(2021, 6, 28, 22, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "AED", + LocalDateTime.of(2021, 7, 20, 14, 0, 0), + LocalDateTime.of(2021, 7, 20, 17, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "AED", + LocalDateTime.of(2021, 9, 16, 19, 0, 0), + LocalDateTime.of(2021, 9, 16, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "ALGA", + LocalDateTime.of(2021, 7, 8, 19, 0, 0), + LocalDateTime.of(2021, 7, 8, 22, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "ALGA", + LocalDateTime.of(2021, 7, 23, 10, 0, 0), + LocalDateTime.of(2021, 7, 23, 13, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "ALGA", + LocalDateTime.of(2021, 9, 8, 19, 0, 0), + LocalDateTime.of(2021, 9, 8, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "AVE", + LocalDateTime.of(2021, 7, 5, 19, 0, 0), + LocalDateTime.of(2021, 7, 5, 22, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "AVE", + LocalDateTime.of(2021, 7, 22, 10, 0, 0), + LocalDateTime.of(2021, 7, 22, 13, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "AVE", + LocalDateTime.of(2021, 9, 15, 14, 0, 0), + LocalDateTime.of(2021, 9, 15, 17, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "CDI", + LocalDateTime.of(2021, 7, 16, 19, 0, 0), + LocalDateTime.of(2021, 7, 16, 22, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "CDI", + LocalDateTime.of(2021, 7, 30, 19, 0, 0), + LocalDateTime.of(2021, 7, 30, 22, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "CDI", + LocalDateTime.of(2021, 9, 17, 19, 0, 0), + LocalDateTime.of(2021, 9, 17, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "CN", + LocalDateTime.of(2021, 7, 15, 10, 0, 0), + LocalDateTime.of(2021, 7, 15, 13, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "CN", + LocalDateTime.of(2021, 7, 29, 19, 0, 0), + LocalDateTime.of(2021, 7, 29, 22, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "CN", + LocalDateTime.of(2021, 9, 10, 19, 0, 0), + LocalDateTime.of(2021, 9, 10, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "COM", + LocalDateTime.of(2021, 7, 12, 19, 0, 0), + LocalDateTime.of(2021, 7, 12, 22, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "COM", + LocalDateTime.of(2021, 7, 26, 14, 0, 0), + LocalDateTime.of(2021, 7, 26, 17, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "COM", + LocalDateTime.of(2021, 9, 13, 19, 0, 0), + LocalDateTime.of(2021, 9, 13, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "CQF", + LocalDateTime.of(2021, 9, 8, 10, 0, 0), + LocalDateTime.of(2021, 9, 8, 13, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "CSM", + LocalDateTime.of(2021, 7, 12, 19, 0, 0), + LocalDateTime.of(2021, 7, 12, 22, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "CSM", + LocalDateTime.of(2021, 7, 31, 10, 0, 0), + LocalDateTime.of(2021, 7, 31, 13, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "CSM", + LocalDateTime.of(2021, 9, 8, 10, 0, 0), + LocalDateTime.of(2021, 9, 8, 13, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "DAW", + LocalDateTime.of(2021, 6, 28, 19, 0, 0), + LocalDateTime.of(2021, 6, 28, 22, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "DAW", + LocalDateTime.of(2021, 7, 21, 10, 0, 0), + LocalDateTime.of(2021, 7, 21, 13, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "DAW", + LocalDateTime.of(2021, 9, 15, 19, 0, 0), + LocalDateTime.of(2021, 9, 15, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "EGP", + LocalDateTime.of(2021, 7, 1, 19, 0, 0), + LocalDateTime.of(2021, 7, 1, 22, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "EGP", + LocalDateTime.of(2021, 7, 20, 14, 0, 0), + LocalDateTime.of(2021, 7, 20, 17, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "EGP", + LocalDateTime.of(2021, 9, 9, 19, 0, 0), + LocalDateTime.of(2021, 9, 9, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "Eltr", + LocalDateTime.of(2021, 7, 13, 19, 0, 0), + LocalDateTime.of(2021, 7, 13, 22, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "Eltr", + LocalDateTime.of(2021, 7, 27, 10, 0, 0), + LocalDateTime.of(2021, 7, 27, 13, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "Eltr", + LocalDateTime.of(2021, 9, 15, 19, 0, 0), + LocalDateTime.of(2021, 9, 15, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "Emp", + LocalDateTime.of(2021, 9, 16, 18, 30), + LocalDateTime.of(2021, 9, 16, 21, 30), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "GAP", + LocalDateTime.of(2021, 6, 30, 10, 0, 0), + LocalDateTime.of(2021, 6, 30, 13, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "GAP", + LocalDateTime.of(2021, 7, 19, 10, 0, 0), + LocalDateTime.of(2021, 7, 19, 13, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "GAP", + LocalDateTime.of(2021, 9, 3, 14, 0, 0), + LocalDateTime.of(2021, 9, 3, 17, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "GQS", + LocalDateTime.of(2021, 9, 7, 18, 30), + LocalDateTime.of(2021, 9, 7, 21, 30), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "IASA", + LocalDateTime.of(2021, 7, 2, 10, 0, 0), + LocalDateTime.of(2021, 7, 2, 13, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "IASA", + LocalDateTime.of(2021, 7, 20, 19, 0, 0), + LocalDateTime.of(2021, 7, 20, 22, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "IASA", + LocalDateTime.of(2021, 9, 2, 19, 0, 0), + LocalDateTime.of(2021, 9, 2, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "IEB", + LocalDateTime.of(2021, 9, 6, 10, 0, 0), + LocalDateTime.of(2021, 9, 6, 13, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "LC", + LocalDateTime.of(2021, 9, 14, 10, 0, 0), + LocalDateTime.of(2021, 9, 14, 13, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "LSD", + LocalDateTime.of(2021, 7, 6, 10, 0, 0), + LocalDateTime.of(2021, 7, 6, 13, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "LSD", + LocalDateTime.of(2021, 7, 21, 19, 0, 0), + LocalDateTime.of(2021, 7, 21, 22, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "LSD", + LocalDateTime.of(2021, 9, 6, 19, 0, 0), + LocalDateTime.of(2021, 9, 6, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "PC", + LocalDateTime.of(2021, 7, 13, 19, 0, 0), + LocalDateTime.of(2021, 7, 13, 22, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "PC", + LocalDateTime.of(2021, 7, 27, 19, 0, 0), + LocalDateTime.of(2021, 7, 27, 22, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "PC", + LocalDateTime.of(2021, 9, 17, 19, 0, 0), + LocalDateTime.of(2021, 9, 17, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "PDM", + LocalDateTime.of(2021, 9, 14, 19, 0, 0), + LocalDateTime.of(2021, 9, 14, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "PE", + LocalDateTime.of(2021, 7, 5, 10, 0, 0), + LocalDateTime.of(2021, 7, 5, 13, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "PE", + LocalDateTime.of(2021, 7, 22, 19, 0, 0), + LocalDateTime.of(2021, 7, 22, 22, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "PE", + LocalDateTime.of(2021, 9, 9, 19, 0, 0), + LocalDateTime.of(2021, 9, 9, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "PG", + LocalDateTime.of(2021, 6, 29, 19, 0, 0), + LocalDateTime.of(2021, 6, 29, 22, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "PG", + LocalDateTime.of(2021, 7, 23, 14, 0, 0), + LocalDateTime.of(2021, 7, 23, 17, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "PG", + LocalDateTime.of(2021, 9, 8, 14, 0, 0), + LocalDateTime.of(2021, 9, 8, 17, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "PI", + LocalDateTime.of(2021, 7, 2, 19, 0, 0), + LocalDateTime.of(2021, 7, 2, 22, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "PI", + LocalDateTime.of(2021, 7, 21, 19, 0, 0), + LocalDateTime.of(2021, 7, 21, 22, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "PI", + LocalDateTime.of(2021, 9, 10, 19, 0, 0), + LocalDateTime.of(2021, 9, 10, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "POO", + LocalDateTime.of(2021, 9, 14, 19, 0, 0), + LocalDateTime.of(2021, 9, 14, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "PSC", + LocalDateTime.of(2021, 7, 6, 19, 0, 0), + LocalDateTime.of(2021, 7, 6, 22, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "PSC", + LocalDateTime.of(2021, 7, 21, 14, 0, 0), + LocalDateTime.of(2021, 7, 21, 17, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "PSC", + LocalDateTime.of(2021, 9, 10, 19, 0, 0), + LocalDateTime.of(2021, 9, 10, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "RCp", + LocalDateTime.of(2021, 7, 14, 10, 0, 0), + LocalDateTime.of(2021, 7, 14, 13, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "RCp", + LocalDateTime.of(2021, 7, 28, 19, 0, 0), + LocalDateTime.of(2021, 7, 28, 22, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "RCp", + LocalDateTime.of(2021, 9, 13, 19, 0, 0), + LocalDateTime.of(2021, 9, 13, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "RI", + LocalDateTime.of(2021, 9, 7, 14, 0, 0), + LocalDateTime.of(2021, 9, 7, 17, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "SE1", + LocalDateTime.of(2021, 9, 6, 19, 0, 0), + LocalDateTime.of(2021, 9, 6, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "SE2", + LocalDateTime.of(2021, 7, 9, 19, 0, 0), + LocalDateTime.of(2021, 7, 9, 22, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "SE2", + LocalDateTime.of(2021, 7, 26, 10, 0, 0), + LocalDateTime.of(2021, 7, 26, 13, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "SE2", + LocalDateTime.of(2021, 9, 6, 19, 0, 0), + LocalDateTime.of(2021, 9, 6, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "SG", + LocalDateTime.of(2021, 7, 1, 10, 0, 0), + LocalDateTime.of(2021, 7, 1, 13, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "SG", + LocalDateTime.of(2021, 7, 20, 19, 0, 0), + LocalDateTime.of(2021, 7, 20, 22, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "SG", + LocalDateTime.of(2021, 9, 8, 14, 0, 0), + LocalDateTime.of(2021, 9, 8, 17, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "SegInf", + LocalDateTime.of(2021, 9, 17, 14, 0, 0), + LocalDateTime.of(2021, 9, 17, 17, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "SI1", + LocalDateTime.of(2021, 6, 30, 19, 0, 0), + LocalDateTime.of(2021, 6, 30, 22, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "SI1", + LocalDateTime.of(2021, 7, 19, 14, 0, 0), + LocalDateTime.of(2021, 7, 19, 17, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "SI1", + LocalDateTime.of(2021, 9, 3, 19, 0, 0), + LocalDateTime.of(2021, 9, 3, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "SI2", + LocalDateTime.of(2021, 7, 8, 14, 0, 0), + LocalDateTime.of(2021, 7, 8, 17, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "SI2", + LocalDateTime.of(2021, 7, 23, 19, 0, 0), + LocalDateTime.of(2021, 7, 23, 22, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "SI2", + LocalDateTime.of(2021, 9, 13, 10, 0, 0), + LocalDateTime.of(2021, 9, 13, 13, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "SM", + LocalDateTime.of(2021, 9, 15, 10, 0, 0), + LocalDateTime.of(2021, 9, 15, 13, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "SO-leic", + LocalDateTime.of(2021, 7, 9, 10, 0, 0), + LocalDateTime.of(2021, 7, 9, 13, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "SO-leic", + LocalDateTime.of(2021, 7, 26, 19, 0, 0), + LocalDateTime.of(2021, 7, 26, 22, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "SO-leic", + LocalDateTime.of(2021, 9, 7, 19, 0, 0), + LocalDateTime.of(2021, 9, 7, 22, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "TAR", + LocalDateTime.of(2021, 7, 16, 10, 0, 0), + LocalDateTime.of(2021, 7, 16, 13, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "TAR", + LocalDateTime.of(2021, 7, 30, 19, 0, 0), + LocalDateTime.of(2021, 7, 30, 22, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "TAR", + LocalDateTime.of(2021, 9, 7, 10, 0, 0), + LocalDateTime.of(2021, 9, 7, 13, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "TJ", + LocalDateTime.of(2021, 7, 12, 10, 0, 0), + LocalDateTime.of(2021, 7, 12, 13, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "TJ", + LocalDateTime.of(2021, 7, 28, 10, 0, 0), + LocalDateTime.of(2021, 7, 28, 13, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "TJ", + LocalDateTime.of(2021, 9, 17, 14, 0, 0), + LocalDateTime.of(2021, 9, 17, 17, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "TMD", + LocalDateTime.of(2021, 7, 10, 10, 0, 0), + LocalDateTime.of(2021, 7, 10, 13, 0, 0), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "TMD", + LocalDateTime.of(2021, 7, 29, 10, 0, 0), + LocalDateTime.of(2021, 7, 29, 13, 0, 0), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "TMD", + LocalDateTime.of(2021, 9, 13, 14, 0, 0), + LocalDateTime.of(2021, 9, 13, 17, 0, 0), + ExamCategory.EXAM_SPECIAL, + "" + ) + ) ) assertEquals(academicCalendarExpected, academicCalendarRetrieved) From 59c7ef7742cd9143f4a49737fc3bd725e3eae26d Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Tue, 6 Jul 2021 17:22:11 +0100 Subject: [PATCH 58/67] Added condition on the Evaluation Job to not delete the test pdf file in the resources. Changed to ZonedDateTime to format date as ISO8601 --- .../application/job/ISELEvaluationsJob.kt | 3 +- .../domain/evaluations/BusinessObjects.kt | 5 +- .../integration/infrastructure/DateUtils.kt | 9 +- ...EvaluationsBusinessObjFormatCheckerTest.kt | 395 +++++++++--------- 4 files changed, 207 insertions(+), 205 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt b/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt index 2c4b0062..132ea39e 100644 --- a/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt +++ b/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt @@ -102,7 +102,8 @@ class ISELEvaluationsJob( ) }.orThrow() } finally { - File(path).delete() + if (!path.contains("test")) + File(path).delete() } } diff --git a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt index 2253847e..ef68d0bf 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt @@ -9,7 +9,6 @@ import org.ionproject.integration.infrastructure.Try import org.ionproject.integration.infrastructure.orThrow import org.ionproject.integration.infrastructure.pdfextractor.tabula.Table import org.ionproject.integration.infrastructure.text.JsonUtils -import java.time.LocalDateTime import java.time.ZonedDateTime data class Evaluations( @@ -137,8 +136,8 @@ data class Evaluations( data class Exam( val course: String, - val startDate: LocalDateTime, - val endDate: LocalDateTime, + val startDate: ZonedDateTime, + val endDate: ZonedDateTime, val category: ExamCategory, val location: String ) diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/DateUtils.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/DateUtils.kt index a3ba0029..69e1d841 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/DateUtils.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/DateUtils.kt @@ -106,8 +106,9 @@ object DateUtils { val duration = LocalTime.parse(durationStr, durationFormat) val date = LocalDate.parse(dayMonthStr.split(".")[0] + " " + yearStr, dateFormat) - val startDateTime = LocalDateTime.of(date, time) - val endDateTime = LocalDateTime.of(date, addToStartTime(time, duration)) + val startDateTime = ZonedDateTime.of(LocalDateTime.of(date, time), ZoneId.systemDefault()) + val endDateTime = + ZonedDateTime.of(LocalDateTime.of(date, addToStartTime(time, duration)), ZoneId.systemDefault()) return IntervalDateTime(startDateTime, endDateTime) } @@ -128,8 +129,8 @@ object DateUtils { } data class IntervalDateTime( - val from: LocalDateTime, - val to: LocalDateTime + val from: ZonedDateTime, + val to: ZonedDateTime ) data class IntervalDate( diff --git a/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt b/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt index c6e927d4..6f644003 100644 --- a/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt +++ b/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt @@ -21,7 +21,8 @@ import org.springframework.batch.core.configuration.annotation.JobBuilderFactory import org.springframework.batch.core.configuration.annotation.StepBuilderFactory import java.io.File import java.net.URI -import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime import javax.sql.DataSource class EvaluationsBusinessObjFormatCheckerTest { @@ -93,687 +94,687 @@ class EvaluationsBusinessObjFormatCheckerTest { listOf( Exam( "AApl", - LocalDateTime.of(2021, 7, 7, 10, 0, 0), - LocalDateTime.of(2021, 7, 7, 13, 0, 0), + ZonedDateTime.of(2021, 7, 7, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 7, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "AApl", - LocalDateTime.of(2021, 7, 24, 10, 0, 0), - LocalDateTime.of(2021, 7, 24, 13, 0, 0), + ZonedDateTime.of(2021, 7, 24, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 24, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "AApl", - LocalDateTime.of(2021, 9, 9, 14, 0, 0), - LocalDateTime.of(2021, 9, 9, 17, 0, 0), + ZonedDateTime.of(2021, 9, 9, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 9, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "AC", - LocalDateTime.of(2021, 7, 1, 14, 0, 0), - LocalDateTime.of(2021, 7, 1, 17, 0, 0), + ZonedDateTime.of(2021, 7, 1, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 1, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "AC", - LocalDateTime.of(2021, 7, 19, 19, 0, 0), - LocalDateTime.of(2021, 7, 19, 22, 0, 0), + ZonedDateTime.of(2021, 7, 19, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 19, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "AC", - LocalDateTime.of(2021, 9, 7, 10, 0, 0), - LocalDateTime.of(2021, 9, 7, 13, 0, 0), + ZonedDateTime.of(2021, 9, 7, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 7, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "AED", - LocalDateTime.of(2021, 6, 28, 19, 0, 0), - LocalDateTime.of(2021, 6, 28, 22, 0, 0), + ZonedDateTime.of(2021, 6, 28, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 6, 28, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "AED", - LocalDateTime.of(2021, 7, 20, 14, 0, 0), - LocalDateTime.of(2021, 7, 20, 17, 0, 0), + ZonedDateTime.of(2021, 7, 20, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 20, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "AED", - LocalDateTime.of(2021, 9, 16, 19, 0, 0), - LocalDateTime.of(2021, 9, 16, 22, 0, 0), + ZonedDateTime.of(2021, 9, 16, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 16, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "ALGA", - LocalDateTime.of(2021, 7, 8, 19, 0, 0), - LocalDateTime.of(2021, 7, 8, 22, 0, 0), + ZonedDateTime.of(2021, 7, 8, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 8, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "ALGA", - LocalDateTime.of(2021, 7, 23, 10, 0, 0), - LocalDateTime.of(2021, 7, 23, 13, 0, 0), + ZonedDateTime.of(2021, 7, 23, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 23, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "ALGA", - LocalDateTime.of(2021, 9, 8, 19, 0, 0), - LocalDateTime.of(2021, 9, 8, 22, 0, 0), + ZonedDateTime.of(2021, 9, 8, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 8, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "AVE", - LocalDateTime.of(2021, 7, 5, 19, 0, 0), - LocalDateTime.of(2021, 7, 5, 22, 0, 0), + ZonedDateTime.of(2021, 7, 5, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 5, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "AVE", - LocalDateTime.of(2021, 7, 22, 10, 0, 0), - LocalDateTime.of(2021, 7, 22, 13, 0, 0), + ZonedDateTime.of(2021, 7, 22, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 22, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "AVE", - LocalDateTime.of(2021, 9, 15, 14, 0, 0), - LocalDateTime.of(2021, 9, 15, 17, 0, 0), + ZonedDateTime.of(2021, 9, 15, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 15, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "CDI", - LocalDateTime.of(2021, 7, 16, 19, 0, 0), - LocalDateTime.of(2021, 7, 16, 22, 0, 0), + ZonedDateTime.of(2021, 7, 16, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 16, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "CDI", - LocalDateTime.of(2021, 7, 30, 19, 0, 0), - LocalDateTime.of(2021, 7, 30, 22, 0, 0), + ZonedDateTime.of(2021, 7, 30, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 30, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "CDI", - LocalDateTime.of(2021, 9, 17, 19, 0, 0), - LocalDateTime.of(2021, 9, 17, 22, 0, 0), + ZonedDateTime.of(2021, 9, 17, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 17, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "CN", - LocalDateTime.of(2021, 7, 15, 10, 0, 0), - LocalDateTime.of(2021, 7, 15, 13, 0, 0), + ZonedDateTime.of(2021, 7, 15, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 15, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "CN", - LocalDateTime.of(2021, 7, 29, 19, 0, 0), - LocalDateTime.of(2021, 7, 29, 22, 0, 0), + ZonedDateTime.of(2021, 7, 29, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 29, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "CN", - LocalDateTime.of(2021, 9, 10, 19, 0, 0), - LocalDateTime.of(2021, 9, 10, 22, 0, 0), + ZonedDateTime.of(2021, 9, 10, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 10, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "COM", - LocalDateTime.of(2021, 7, 12, 19, 0, 0), - LocalDateTime.of(2021, 7, 12, 22, 0, 0), + ZonedDateTime.of(2021, 7, 12, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 12, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "COM", - LocalDateTime.of(2021, 7, 26, 14, 0, 0), - LocalDateTime.of(2021, 7, 26, 17, 0, 0), + ZonedDateTime.of(2021, 7, 26, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 26, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "COM", - LocalDateTime.of(2021, 9, 13, 19, 0, 0), - LocalDateTime.of(2021, 9, 13, 22, 0, 0), + ZonedDateTime.of(2021, 9, 13, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 13, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "CQF", - LocalDateTime.of(2021, 9, 8, 10, 0, 0), - LocalDateTime.of(2021, 9, 8, 13, 0, 0), + ZonedDateTime.of(2021, 9, 8, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 8, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "CSM", - LocalDateTime.of(2021, 7, 12, 19, 0, 0), - LocalDateTime.of(2021, 7, 12, 22, 0, 0), + ZonedDateTime.of(2021, 7, 12, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 12, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "CSM", - LocalDateTime.of(2021, 7, 31, 10, 0, 0), - LocalDateTime.of(2021, 7, 31, 13, 0, 0), + ZonedDateTime.of(2021, 7, 31, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 31, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "CSM", - LocalDateTime.of(2021, 9, 8, 10, 0, 0), - LocalDateTime.of(2021, 9, 8, 13, 0, 0), + ZonedDateTime.of(2021, 9, 8, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 8, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "DAW", - LocalDateTime.of(2021, 6, 28, 19, 0, 0), - LocalDateTime.of(2021, 6, 28, 22, 0, 0), + ZonedDateTime.of(2021, 6, 28, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 6, 28, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "DAW", - LocalDateTime.of(2021, 7, 21, 10, 0, 0), - LocalDateTime.of(2021, 7, 21, 13, 0, 0), + ZonedDateTime.of(2021, 7, 21, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 21, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "DAW", - LocalDateTime.of(2021, 9, 15, 19, 0, 0), - LocalDateTime.of(2021, 9, 15, 22, 0, 0), + ZonedDateTime.of(2021, 9, 15, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 15, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "EGP", - LocalDateTime.of(2021, 7, 1, 19, 0, 0), - LocalDateTime.of(2021, 7, 1, 22, 0, 0), + ZonedDateTime.of(2021, 7, 1, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 1, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "EGP", - LocalDateTime.of(2021, 7, 20, 14, 0, 0), - LocalDateTime.of(2021, 7, 20, 17, 0, 0), + ZonedDateTime.of(2021, 7, 20, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 20, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "EGP", - LocalDateTime.of(2021, 9, 9, 19, 0, 0), - LocalDateTime.of(2021, 9, 9, 22, 0, 0), + ZonedDateTime.of(2021, 9, 9, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 9, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "Eltr", - LocalDateTime.of(2021, 7, 13, 19, 0, 0), - LocalDateTime.of(2021, 7, 13, 22, 0, 0), + ZonedDateTime.of(2021, 7, 13, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 13, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "Eltr", - LocalDateTime.of(2021, 7, 27, 10, 0, 0), - LocalDateTime.of(2021, 7, 27, 13, 0, 0), + ZonedDateTime.of(2021, 7, 27, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 27, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "Eltr", - LocalDateTime.of(2021, 9, 15, 19, 0, 0), - LocalDateTime.of(2021, 9, 15, 22, 0, 0), + ZonedDateTime.of(2021, 9, 15, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 15, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "Emp", - LocalDateTime.of(2021, 9, 16, 18, 30), - LocalDateTime.of(2021, 9, 16, 21, 30), + ZonedDateTime.of(2021, 9, 16, 18, 30, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 16, 21, 30, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "GAP", - LocalDateTime.of(2021, 6, 30, 10, 0, 0), - LocalDateTime.of(2021, 6, 30, 13, 0, 0), + ZonedDateTime.of(2021, 6, 30, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 6, 30, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "GAP", - LocalDateTime.of(2021, 7, 19, 10, 0, 0), - LocalDateTime.of(2021, 7, 19, 13, 0, 0), + ZonedDateTime.of(2021, 7, 19, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 19, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "GAP", - LocalDateTime.of(2021, 9, 3, 14, 0, 0), - LocalDateTime.of(2021, 9, 3, 17, 0, 0), + ZonedDateTime.of(2021, 9, 3, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 3, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "GQS", - LocalDateTime.of(2021, 9, 7, 18, 30), - LocalDateTime.of(2021, 9, 7, 21, 30), + ZonedDateTime.of(2021, 9, 7, 18, 30, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 7, 21, 30, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "IASA", - LocalDateTime.of(2021, 7, 2, 10, 0, 0), - LocalDateTime.of(2021, 7, 2, 13, 0, 0), + ZonedDateTime.of(2021, 7, 2, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 2, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "IASA", - LocalDateTime.of(2021, 7, 20, 19, 0, 0), - LocalDateTime.of(2021, 7, 20, 22, 0, 0), + ZonedDateTime.of(2021, 7, 20, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 20, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "IASA", - LocalDateTime.of(2021, 9, 2, 19, 0, 0), - LocalDateTime.of(2021, 9, 2, 22, 0, 0), + ZonedDateTime.of(2021, 9, 2, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 2, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "IEB", - LocalDateTime.of(2021, 9, 6, 10, 0, 0), - LocalDateTime.of(2021, 9, 6, 13, 0, 0), + ZonedDateTime.of(2021, 9, 6, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 6, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "LC", - LocalDateTime.of(2021, 9, 14, 10, 0, 0), - LocalDateTime.of(2021, 9, 14, 13, 0, 0), + ZonedDateTime.of(2021, 9, 14, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 14, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "LSD", - LocalDateTime.of(2021, 7, 6, 10, 0, 0), - LocalDateTime.of(2021, 7, 6, 13, 0, 0), + ZonedDateTime.of(2021, 7, 6, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 6, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "LSD", - LocalDateTime.of(2021, 7, 21, 19, 0, 0), - LocalDateTime.of(2021, 7, 21, 22, 0, 0), + ZonedDateTime.of(2021, 7, 21, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 21, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "LSD", - LocalDateTime.of(2021, 9, 6, 19, 0, 0), - LocalDateTime.of(2021, 9, 6, 22, 0, 0), + ZonedDateTime.of(2021, 9, 6, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 6, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "PC", - LocalDateTime.of(2021, 7, 13, 19, 0, 0), - LocalDateTime.of(2021, 7, 13, 22, 0, 0), + ZonedDateTime.of(2021, 7, 13, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 13, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "PC", - LocalDateTime.of(2021, 7, 27, 19, 0, 0), - LocalDateTime.of(2021, 7, 27, 22, 0, 0), + ZonedDateTime.of(2021, 7, 27, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 27, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "PC", - LocalDateTime.of(2021, 9, 17, 19, 0, 0), - LocalDateTime.of(2021, 9, 17, 22, 0, 0), + ZonedDateTime.of(2021, 9, 17, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 17, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "PDM", - LocalDateTime.of(2021, 9, 14, 19, 0, 0), - LocalDateTime.of(2021, 9, 14, 22, 0, 0), + ZonedDateTime.of(2021, 9, 14, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 14, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "PE", - LocalDateTime.of(2021, 7, 5, 10, 0, 0), - LocalDateTime.of(2021, 7, 5, 13, 0, 0), + ZonedDateTime.of(2021, 7, 5, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 5, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "PE", - LocalDateTime.of(2021, 7, 22, 19, 0, 0), - LocalDateTime.of(2021, 7, 22, 22, 0, 0), + ZonedDateTime.of(2021, 7, 22, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 22, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "PE", - LocalDateTime.of(2021, 9, 9, 19, 0, 0), - LocalDateTime.of(2021, 9, 9, 22, 0, 0), + ZonedDateTime.of(2021, 9, 9, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 9, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "PG", - LocalDateTime.of(2021, 6, 29, 19, 0, 0), - LocalDateTime.of(2021, 6, 29, 22, 0, 0), + ZonedDateTime.of(2021, 6, 29, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 6, 29, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "PG", - LocalDateTime.of(2021, 7, 23, 14, 0, 0), - LocalDateTime.of(2021, 7, 23, 17, 0, 0), + ZonedDateTime.of(2021, 7, 23, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 23, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "PG", - LocalDateTime.of(2021, 9, 8, 14, 0, 0), - LocalDateTime.of(2021, 9, 8, 17, 0, 0), + ZonedDateTime.of(2021, 9, 8, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 8, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "PI", - LocalDateTime.of(2021, 7, 2, 19, 0, 0), - LocalDateTime.of(2021, 7, 2, 22, 0, 0), + ZonedDateTime.of(2021, 7, 2, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 2, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "PI", - LocalDateTime.of(2021, 7, 21, 19, 0, 0), - LocalDateTime.of(2021, 7, 21, 22, 0, 0), + ZonedDateTime.of(2021, 7, 21, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 21, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "PI", - LocalDateTime.of(2021, 9, 10, 19, 0, 0), - LocalDateTime.of(2021, 9, 10, 22, 0, 0), + ZonedDateTime.of(2021, 9, 10, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 10, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "POO", - LocalDateTime.of(2021, 9, 14, 19, 0, 0), - LocalDateTime.of(2021, 9, 14, 22, 0, 0), + ZonedDateTime.of(2021, 9, 14, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 14, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "PSC", - LocalDateTime.of(2021, 7, 6, 19, 0, 0), - LocalDateTime.of(2021, 7, 6, 22, 0, 0), + ZonedDateTime.of(2021, 7, 6, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 6, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "PSC", - LocalDateTime.of(2021, 7, 21, 14, 0, 0), - LocalDateTime.of(2021, 7, 21, 17, 0, 0), + ZonedDateTime.of(2021, 7, 21, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 21, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "PSC", - LocalDateTime.of(2021, 9, 10, 19, 0, 0), - LocalDateTime.of(2021, 9, 10, 22, 0, 0), + ZonedDateTime.of(2021, 9, 10, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 10, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "RCp", - LocalDateTime.of(2021, 7, 14, 10, 0, 0), - LocalDateTime.of(2021, 7, 14, 13, 0, 0), + ZonedDateTime.of(2021, 7, 14, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 14, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "RCp", - LocalDateTime.of(2021, 7, 28, 19, 0, 0), - LocalDateTime.of(2021, 7, 28, 22, 0, 0), + ZonedDateTime.of(2021, 7, 28, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 28, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "RCp", - LocalDateTime.of(2021, 9, 13, 19, 0, 0), - LocalDateTime.of(2021, 9, 13, 22, 0, 0), + ZonedDateTime.of(2021, 9, 13, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 13, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "RI", - LocalDateTime.of(2021, 9, 7, 14, 0, 0), - LocalDateTime.of(2021, 9, 7, 17, 0, 0), + ZonedDateTime.of(2021, 9, 7, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 7, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "SE1", - LocalDateTime.of(2021, 9, 6, 19, 0, 0), - LocalDateTime.of(2021, 9, 6, 22, 0, 0), + ZonedDateTime.of(2021, 9, 6, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 6, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "SE2", - LocalDateTime.of(2021, 7, 9, 19, 0, 0), - LocalDateTime.of(2021, 7, 9, 22, 0, 0), + ZonedDateTime.of(2021, 7, 9, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 9, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "SE2", - LocalDateTime.of(2021, 7, 26, 10, 0, 0), - LocalDateTime.of(2021, 7, 26, 13, 0, 0), + ZonedDateTime.of(2021, 7, 26, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 26, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "SE2", - LocalDateTime.of(2021, 9, 6, 19, 0, 0), - LocalDateTime.of(2021, 9, 6, 22, 0, 0), + ZonedDateTime.of(2021, 9, 6, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 6, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "SG", - LocalDateTime.of(2021, 7, 1, 10, 0, 0), - LocalDateTime.of(2021, 7, 1, 13, 0, 0), + ZonedDateTime.of(2021, 7, 1, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 1, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "SG", - LocalDateTime.of(2021, 7, 20, 19, 0, 0), - LocalDateTime.of(2021, 7, 20, 22, 0, 0), + ZonedDateTime.of(2021, 7, 20, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 20, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "SG", - LocalDateTime.of(2021, 9, 8, 14, 0, 0), - LocalDateTime.of(2021, 9, 8, 17, 0, 0), + ZonedDateTime.of(2021, 9, 8, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 8, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "SegInf", - LocalDateTime.of(2021, 9, 17, 14, 0, 0), - LocalDateTime.of(2021, 9, 17, 17, 0, 0), + ZonedDateTime.of(2021, 9, 17, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 17, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "SI1", - LocalDateTime.of(2021, 6, 30, 19, 0, 0), - LocalDateTime.of(2021, 6, 30, 22, 0, 0), + ZonedDateTime.of(2021, 6, 30, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 6, 30, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "SI1", - LocalDateTime.of(2021, 7, 19, 14, 0, 0), - LocalDateTime.of(2021, 7, 19, 17, 0, 0), + ZonedDateTime.of(2021, 7, 19, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 19, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "SI1", - LocalDateTime.of(2021, 9, 3, 19, 0, 0), - LocalDateTime.of(2021, 9, 3, 22, 0, 0), + ZonedDateTime.of(2021, 9, 3, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 3, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "SI2", - LocalDateTime.of(2021, 7, 8, 14, 0, 0), - LocalDateTime.of(2021, 7, 8, 17, 0, 0), + ZonedDateTime.of(2021, 7, 8, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 8, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "SI2", - LocalDateTime.of(2021, 7, 23, 19, 0, 0), - LocalDateTime.of(2021, 7, 23, 22, 0, 0), + ZonedDateTime.of(2021, 7, 23, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 23, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "SI2", - LocalDateTime.of(2021, 9, 13, 10, 0, 0), - LocalDateTime.of(2021, 9, 13, 13, 0, 0), + ZonedDateTime.of(2021, 9, 13, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 13, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "SM", - LocalDateTime.of(2021, 9, 15, 10, 0, 0), - LocalDateTime.of(2021, 9, 15, 13, 0, 0), + ZonedDateTime.of(2021, 9, 15, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 15, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "SO-leic", - LocalDateTime.of(2021, 7, 9, 10, 0, 0), - LocalDateTime.of(2021, 7, 9, 13, 0, 0), + ZonedDateTime.of(2021, 7, 9, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 9, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "SO-leic", - LocalDateTime.of(2021, 7, 26, 19, 0, 0), - LocalDateTime.of(2021, 7, 26, 22, 0, 0), + ZonedDateTime.of(2021, 7, 26, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 26, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "SO-leic", - LocalDateTime.of(2021, 9, 7, 19, 0, 0), - LocalDateTime.of(2021, 9, 7, 22, 0, 0), + ZonedDateTime.of(2021, 9, 7, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 7, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "TAR", - LocalDateTime.of(2021, 7, 16, 10, 0, 0), - LocalDateTime.of(2021, 7, 16, 13, 0, 0), + ZonedDateTime.of(2021, 7, 16, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 16, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "TAR", - LocalDateTime.of(2021, 7, 30, 19, 0, 0), - LocalDateTime.of(2021, 7, 30, 22, 0, 0), + ZonedDateTime.of(2021, 7, 30, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 30, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "TAR", - LocalDateTime.of(2021, 9, 7, 10, 0, 0), - LocalDateTime.of(2021, 9, 7, 13, 0, 0), + ZonedDateTime.of(2021, 9, 7, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 7, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "TJ", - LocalDateTime.of(2021, 7, 12, 10, 0, 0), - LocalDateTime.of(2021, 7, 12, 13, 0, 0), + ZonedDateTime.of(2021, 7, 12, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 12, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "TJ", - LocalDateTime.of(2021, 7, 28, 10, 0, 0), - LocalDateTime.of(2021, 7, 28, 13, 0, 0), + ZonedDateTime.of(2021, 7, 28, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 28, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "TJ", - LocalDateTime.of(2021, 9, 17, 14, 0, 0), - LocalDateTime.of(2021, 9, 17, 17, 0, 0), + ZonedDateTime.of(2021, 9, 17, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 17, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ), Exam( "TMD", - LocalDateTime.of(2021, 7, 10, 10, 0, 0), - LocalDateTime.of(2021, 7, 10, 13, 0, 0), + ZonedDateTime.of(2021, 7, 10, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 10, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( "TMD", - LocalDateTime.of(2021, 7, 29, 10, 0, 0), - LocalDateTime.of(2021, 7, 29, 13, 0, 0), + ZonedDateTime.of(2021, 7, 29, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 29, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( "TMD", - LocalDateTime.of(2021, 9, 13, 14, 0, 0), - LocalDateTime.of(2021, 9, 13, 17, 0, 0), + ZonedDateTime.of(2021, 9, 13, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 13, 17, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, "" ) From 7f229f042b52ea77438bf97ef0bc7506ae85a4e5 Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Tue, 6 Jul 2021 22:39:01 +0100 Subject: [PATCH 59/67] Build of CalendarTerm and retrieval of calendar Term --- .../domain/evaluations/BusinessObjects.kt | 55 ++++++++++++++----- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt index ef68d0bf..331a1e62 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt @@ -9,6 +9,7 @@ import org.ionproject.integration.infrastructure.Try import org.ionproject.integration.infrastructure.orThrow import org.ionproject.integration.infrastructure.pdfextractor.tabula.Table import org.ionproject.integration.infrastructure.text.JsonUtils +import org.ionproject.integration.infrastructure.text.RegexUtils import java.time.ZonedDateTime data class Evaluations( @@ -20,13 +21,16 @@ data class Evaluations( val exams: List ) { companion object { - private const val CALENDAR_TERM_REGEX = "(\\sAno\\sLetivo\\s?:\\s?)(.+?(\\r|\\R))" + private const val CALENDAR_TERM_REGEX = "(verão|inverno) \\d{4}\\/\\d{4}" + private const val SUMMER_TERM = "verão" + private const val WINTER_TERM = "inverno" fun from(rawEvaluationsData: RawEvaluationsData, jobProgramme: ProgrammeModel): Evaluations { + val calendarTerm = buildCalendarTerm(rawEvaluationsData) return Evaluations( - creationDateTime = rawEvaluationsData.creationDate, - retrievalDateTime = DateUtils.formatToISO8601(ZonedDateTime.now()), + rawEvaluationsData.creationDate, + DateUtils.formatToISO8601(ZonedDateTime.now()), School( jobProgramme.institutionModel.name, jobProgramme.institutionModel.acronym @@ -35,23 +39,46 @@ data class Evaluations( jobProgramme.name, jobProgramme.acronym ), - calendarTerm = buildCalendarTerm(rawEvaluationsData), - buildExamList(rawEvaluationsData, jobProgramme) + calendarTerm, + buildExamList(rawEvaluationsData, jobProgramme, getCalendarYear(calendarTerm)) ) } - // TODO - private fun buildCalendarTerm(rawEvaluationsData: RawEvaluationsData): String = "2020-2021-2" + private fun getCalendarYear(calendarTerm: String) = + when (val termNumber = calendarTerm.last()) { + '1' -> calendarTerm.take(4) + '2' -> calendarTerm.substring(5, 9) + else -> throw IllegalArgumentException("Invalid term description: $termNumber") + } + + private fun buildCalendarTerm(rawEvaluationsData: RawEvaluationsData): String { + val calendarTerm = + RegexUtils.findMatches(CALENDAR_TERM_REGEX, rawEvaluationsData.textData.toString()).first() + .replace("/", "-") + + val termNumber = when (val termType = calendarTerm.substringBefore(" ").lowercase()) { + WINTER_TERM -> 1 + SUMMER_TERM -> 2 + else -> throw IllegalArgumentException("Invalid term description: $termType") + } + + return calendarTerm.substringAfter(" ") + "-" + termNumber + } - private fun buildExamList(rawEvaluationsData: RawEvaluationsData, jobProgramme: ProgrammeModel): List = - rawEvaluationsData.table.toTableList().map { getExamsFromTable(it, jobProgramme.acronym) }.orThrow() + private fun buildExamList( + rawEvaluationsData: RawEvaluationsData, + jobProgramme: ProgrammeModel, + year: String + ): List = + rawEvaluationsData.table.toTableList().map { getExamsFromTable(it, jobProgramme.acronym, year) }.orThrow() private fun String.toTableList(): Try> = JsonUtils.fromJson(this, Types.newParameterizedType(List::class.java, Table::class.java)) private fun getExamsFromTable( tableList: List
, - programmeAcronym: String + programmeAcronym: String, + year: String ): List { val examList = mutableListOf() for (table in tableList) { @@ -60,7 +87,7 @@ data class Evaluations( if (cleanedLine[TableColumn.SUMMER_EXAM_PROGRAMME.ordinal].text.contains(programmeAcronym)) { val intervalDateTimeNormal = DateUtils.getEvaluationDateTimeFrom( - "2021", + year, cleanedLine[TableColumn.NORMAL_EXAM_DATE.ordinal].text, cleanedLine[TableColumn.NORMAL_EXAM_TIME.ordinal].text, cleanedLine[TableColumn.NORMAL_EXAM_DURATION.ordinal].text @@ -76,7 +103,7 @@ data class Evaluations( ) val intervalDateTimeAltern = DateUtils.getEvaluationDateTimeFrom( - "2021", + year, cleanedLine[TableColumn.ALTERN_EXAM_DATE.ordinal].text, cleanedLine[TableColumn.ALTERN_EXAM_TIME.ordinal].text, cleanedLine[TableColumn.ALTERN_EXAM_DURATION.ordinal].text @@ -92,7 +119,7 @@ data class Evaluations( ) val intervalDateTimeSpecial = DateUtils.getEvaluationDateTimeFrom( - "2021", + year, cleanedLine[TableColumn.SPECIAL_EXAM_DATE.ordinal].text, cleanedLine[TableColumn.SPECIAL_EXAM_TIME.ordinal].text, cleanedLine[TableColumn.SPECIAL_EXAM_DURATION.ordinal].text @@ -112,7 +139,7 @@ data class Evaluations( ) { val intervalDateTimeSpecial = DateUtils.getEvaluationDateTimeFrom( - "2021", + year, cleanedLine[TableColumnWinterCourse.SPECIAL_EXAM_DATE.ordinal].text, cleanedLine[TableColumnWinterCourse.SPECIAL_EXAM_TIME.ordinal].text, cleanedLine[TableColumnWinterCourse.SPECIAL_EXAM_DURATION.ordinal].text From 9c15c5a2666ebcfccba4c885ff409177c7ca7f2b Mon Sep 17 00:00:00 2001 From: Grimord Date: Wed, 7 Jul 2021 00:33:41 +0100 Subject: [PATCH 60/67] rebase on top of master Co-Authored-By: Ricardo Canto --- .../integration/application/JobEngine.kt | 5 ++-- .../job/ISELAcademicCalendarJob.kt | 27 ++++--------------- .../application/job/ISELTimetableJob.kt | 4 ++- .../application/job/NotificationListener.kt | 8 +++--- .../integration/infrastructure/DateUtils.kt | 21 ++++++++------- .../repository/InstitutionRepositoryImpl.kt | 12 +++++++-- .../ui/controller/JobController.kt | 2 +- .../config/supported-institutions.yml | 14 +++++++++- .../integration/ui/dto/InputProcessorTests.kt | 3 ++- .../integration/utils/DateUtilsTests.kt | 8 +++--- 10 files changed, 57 insertions(+), 47 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt b/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt index 49e7760c..5a3ebe1a 100644 --- a/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt +++ b/src/main/kotlin/org/ionproject/integration/application/JobEngine.kt @@ -2,6 +2,7 @@ package org.ionproject.integration.application import org.ionproject.integration.application.config.AppProperties import org.ionproject.integration.application.config.LAUNCHER_NAME +import org.ionproject.integration.application.job.CALENDAR_JOB_NAME import org.ionproject.integration.application.job.EVALUATIONS_JOB_NAME import org.ionproject.integration.application.job.JobType import org.ionproject.integration.application.job.TIMETABLE_JOB_NAME @@ -69,8 +70,8 @@ class JobEngine( } private fun runCalendarJob(request: CalendarJobRequest): JobStatus { - val jobParams = getJobParameters(request, EVALUATIONS_JOB_NAME) - return runJob(EVALUATIONS_JOB_NAME, jobParams) + val jobParams = getJobParameters(request, CALENDAR_JOB_NAME) + return runJob(CALENDAR_JOB_NAME, jobParams) } private fun getJobParameters(request: AbstractJobRequest, jobName: String): JobParameters { diff --git a/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt b/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt index 128a575d..c3985a2a 100644 --- a/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt +++ b/src/main/kotlin/org/ionproject/integration/application/job/ISELAcademicCalendarJob.kt @@ -21,7 +21,6 @@ import org.ionproject.integration.infrastructure.pdfextractor.ITextPdfExtractor import org.ionproject.integration.infrastructure.pdfextractor.PDFBytesFormatChecker import org.ionproject.integration.infrastructure.repository.IInstitutionRepository import org.ionproject.integration.model.external.calendar.AcademicCalendar -import org.ionproject.integration.model.external.calendar.AcademicCalendarDto import org.springframework.batch.core.ExitStatus import org.springframework.batch.core.configuration.annotation.JobBuilderFactory import org.springframework.batch.core.configuration.annotation.StepBuilderFactory @@ -53,14 +52,15 @@ class ISELAcademicCalendarJob( @Bean(name = [CALENDAR_JOB_NAME]) fun calendarJob() = jobBuilderFactory.get(CALENDAR_JOB_NAME) - .start(taskletStep("Download And Compare", downloadCalendarPDFAlternateTasklet())) + .start(taskletStep("Download And Compare", downloadCalendarPDFTasklet())) .on("STOPPED").end() .next(extractCalendarPDFTasklet()) .next(createCalendarPDFBusinessObjectsTasklet()) .next(createCalendarPDFDtoTasklet()) .next(writeCalendarDTOToGitTasklet()) - .next(sendNotificationsForCalendarJobTasklet()) - .build().build() + .build() + .listener(NotificationListener()) + .build() private fun taskletStep(name: String, tasklet: Tasklet): TaskletStep { return stepBuilderFactory @@ -71,21 +71,12 @@ class ISELAcademicCalendarJob( @StepScope @Bean - fun downloadCalendarPDFAlternateTasklet(): DownloadAndCompareTasklet { + fun downloadCalendarPDFTasklet(): DownloadAndCompareTasklet { val pdfChecker = PDFBytesFormatChecker() val fileComparator = FileComparatorImpl(FileDigestImpl(), HashRepositoryImpl(ds)) return DownloadAndCompareTasklet(downloader, pdfChecker, fileComparator) } - @Bean - fun downloadCalendarPDFTasklet() = stepBuilderFactory.get("Download Calendar PDF") - .tasklet { stepContribution, chunkContext -> - val pdfChecker = PDFBytesFormatChecker() - val fileComparator = FileComparatorImpl(FileDigestImpl(), HashRepositoryImpl(ds)) - DownloadAndCompareTasklet(downloader, pdfChecker, fileComparator).execute(stepContribution, chunkContext) - } - .build() - @Bean fun extractCalendarPDFTasklet() = stepBuilderFactory.get("Extract Calendar PDF Raw Data") .tasklet { stepContribution, _ -> @@ -157,14 +148,6 @@ class ISELAcademicCalendarJob( } .build() - @Bean - fun sendNotificationsForCalendarJobTasklet() = stepBuilderFactory.get("Send Calendar Job Notifications") - .tasklet { _, _ -> - // TODO - RepeatStatus.FINISHED - } - .build() - @Component object State { lateinit var rawCalendarData: RawCalendarData diff --git a/src/main/kotlin/org/ionproject/integration/application/job/ISELTimetableJob.kt b/src/main/kotlin/org/ionproject/integration/application/job/ISELTimetableJob.kt index 307b348b..a7e10b37 100644 --- a/src/main/kotlin/org/ionproject/integration/application/job/ISELTimetableJob.kt +++ b/src/main/kotlin/org/ionproject/integration/application/job/ISELTimetableJob.kt @@ -54,7 +54,9 @@ class ISELTimetableJob( .next(taskletStep("RawData to Business Object", mappingTasklet())) .next(writeLocalStep()) .next(taskletStep("PostUpload", postUploadTasklet())) - .build().build() + .build() + .listener(NotificationListener()) + .build() private fun taskletStep(name: String, tasklet: Tasklet): TaskletStep { return stepBuilderFactory diff --git a/src/main/kotlin/org/ionproject/integration/application/job/NotificationListener.kt b/src/main/kotlin/org/ionproject/integration/application/job/NotificationListener.kt index b447dc97..99123d45 100644 --- a/src/main/kotlin/org/ionproject/integration/application/job/NotificationListener.kt +++ b/src/main/kotlin/org/ionproject/integration/application/job/NotificationListener.kt @@ -11,14 +11,14 @@ class NotificationListener : JobExecutionListener { private val log: Logger = LoggerFactory.getLogger(NotificationListener::class.java) override fun beforeJob(jobExecution: JobExecution) { - log.info("Job ${jobExecution.jobConfigurationName} starting") + log.info("Job ID ${jobExecution.jobId} starting") } override fun afterJob(jobExecution: JobExecution) { when (jobExecution.exitStatus) { - ExitStatus.FAILED -> log.error("Job ${jobExecution.jobConfigurationName} failed") - ExitStatus.COMPLETED -> log.info("Job ${jobExecution.jobConfigurationName} completed") - else -> log.debug("Job ${jobExecution.jobConfigurationName} exited with status = ${jobExecution.status}") + ExitStatus.FAILED -> log.error("Job ID ${jobExecution.jobId} failed") + ExitStatus.COMPLETED -> log.info("Job ID ${jobExecution.jobId} completed") + else -> log.debug("Job ID ${jobExecution.jobId} exited with status = ${jobExecution.status}") } } } diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/DateUtils.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/DateUtils.kt index 69e1d841..5a77c8a0 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/DateUtils.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/DateUtils.kt @@ -93,22 +93,25 @@ object DateUtils { } fun getEvaluationDateTimeFrom( - yearStr: String, - dayMonthStr: String, - timeStr: String, - durationStr: String + year: String, + dayMonth: String, // Format: 15 Jul.(Quinta) + time: String, + duration: String ): IntervalDateTime { val timeFormat = DateTimeFormatter.ofPattern("HH'h'mm") val durationFormat = DateTimeFormatter.ofPattern("H'h'mm") val dateFormat = DateTimeFormatter.ofPattern("d MMM yyyy", localePT) - val time = LocalTime.parse(timeStr, timeFormat) - val duration = LocalTime.parse(durationStr, durationFormat) - val date = LocalDate.parse(dayMonthStr.split(".")[0] + " " + yearStr, dateFormat) + val timeParsed = LocalTime.parse(time, timeFormat) + val durationParsed = LocalTime.parse(duration, durationFormat) - val startDateTime = ZonedDateTime.of(LocalDateTime.of(date, time), ZoneId.systemDefault()) + // Format: d MMM yyyy - Ex.: 15 Jul 2021 + val rawDate = "${dayMonth.split(".").first()} $year" + val date = LocalDate.parse(rawDate, dateFormat) + + val startDateTime = ZonedDateTime.of(LocalDateTime.of(date, timeParsed), ZoneId.systemDefault()) val endDateTime = - ZonedDateTime.of(LocalDateTime.of(date, addToStartTime(time, duration)), ZoneId.systemDefault()) + ZonedDateTime.of(LocalDateTime.of(date, addToStartTime(timeParsed, durationParsed)), ZoneId.systemDefault()) return IntervalDateTime(startDateTime, endDateTime) } diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/repository/InstitutionRepositoryImpl.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/repository/InstitutionRepositoryImpl.kt index f30d3193..9f101008 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/repository/InstitutionRepositoryImpl.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/repository/InstitutionRepositoryImpl.kt @@ -6,6 +6,7 @@ import org.ionproject.integration.application.config.AppProperties import org.ionproject.integration.domain.common.InstitutionModel import org.ionproject.integration.domain.common.Language import org.ionproject.integration.domain.common.ProgrammeModel +import org.ionproject.integration.domain.common.ProgrammeResources import org.ionproject.integration.infrastructure.exception.ArgumentException import org.ionproject.integration.infrastructure.text.IgnoredWords import org.ionproject.integration.infrastructure.text.generateAcronym @@ -68,7 +69,8 @@ internal data class ResourceDto( internal enum class ResourceType(val identifier: String) { CALENDAR("academic_calendar"), - TIMETABLE("timetable") + TIMETABLE("timetable"), + EVALUATIONS("evaluations") } internal data class ProgrammeDto( @@ -79,12 +81,18 @@ internal data class ProgrammeDto( fun toModel(institution: InstitutionModel): ProgrammeModel { val acronym = generateAcronym(name, IgnoredWords.of(Language.PT)) val timetableUri = resources.first { it.type == ResourceType.TIMETABLE.identifier } + val evaluationsUri = resources.first { it.type == ResourceType.EVALUATIONS.identifier } + + val resources = ProgrammeResources( + timetableUri = URI(timetableUri.uri), + evaluationsUri = URI(evaluationsUri.uri) + ) return ProgrammeModel( institutionModel = institution, name = this.name, acronym = acronym, - timetableUri = URI(timetableUri.uri) + resources = resources ) } } diff --git a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt index 46126b08..7f00af24 100644 --- a/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt +++ b/src/main/kotlin/org/ionproject/integration/ui/controller/JobController.kt @@ -29,7 +29,7 @@ class JobController( private val logger = LoggerFactory.getLogger(JobController::class.java) @PostMapping(consumes = ["application/json"]) - fun createTimetableJob( + fun createJob( @RequestBody body: CreateJobDto, servletRequest: HttpServletRequest, response: HttpServletResponse diff --git a/src/main/resources/config/supported-institutions.yml b/src/main/resources/config/supported-institutions.yml index 72c194cc..ae4eda75 100644 --- a/src/main/resources/config/supported-institutions.yml +++ b/src/main/resources/config/supported-institutions.yml @@ -8,23 +8,35 @@ resources: - type: timetable uri: https://www.isel.pt/media/uploads/ADEETC_LEETC_210322.pdf + - type: evaluations + uri: https://www.isel.pt/media/uploads/tinymce/ADEETC_EN_ER_EE_210526.pdf - name: Licenciatura em Engenharia Informática e de Computadores resources: - type: timetable uri: https://www.isel.pt/media/uploads/ADEETC_LEIC_210322.pdf + - type: evaluations + uri: https://www.isel.pt/media/uploads/tinymce/ADEETC_EN_ER_EE_210526.pdf - name: Licenciatura em Engenharia Informática e Multimédia resources: - type: timetable uri: https://www.isel.pt/media/uploads/ADEETC_LEIM_210228.pdf + - type: evaluations + uri: https://www.isel.pt/media/uploads/tinymce/ADEETC_EN_ER_EE_210526.pdf - name: Licenciatura em Engenharia Informática, Redes e Telecomunicações resources: - type: timetable uri: https://www.isel.pt/media/uploads/ADEETC_LEIRT_210318.pdf + - type: evaluations + uri: https://www.isel.pt/media/uploads/tinymce/ADEETC_EN_ER_EE_210526.pdf - name: Mestrado em Engenharia Informática e de Computadores resources: - type: timetable uri: https://www.isel.pt/media/uploads/ADEETC_MEIC_210301.pdf + - type: evaluations + uri: https://www.isel.pt/media/uploads/tinymce/ADEETC_EN_ER_EE_210526.pdf - name: Mestrado em Engenharia Informática e Multimédia resources: - type: timetable - uri: https://www.isel.pt/media/uploads/ADEETC_MEIM_210228.pdf \ No newline at end of file + uri: https://www.isel.pt/media/uploads/ADEETC_MEIM_210228.pdf + - type: evaluations + uri: https://www.isel.pt/media/uploads/tinymce/ADEETC_EN_ER_EE_210526.pdf \ No newline at end of file diff --git a/src/test/kotlin/org/ionproject/integration/ui/dto/InputProcessorTests.kt b/src/test/kotlin/org/ionproject/integration/ui/dto/InputProcessorTests.kt index 53591801..72150a6b 100644 --- a/src/test/kotlin/org/ionproject/integration/ui/dto/InputProcessorTests.kt +++ b/src/test/kotlin/org/ionproject/integration/ui/dto/InputProcessorTests.kt @@ -4,6 +4,7 @@ import org.ionproject.integration.application.JobEngine import org.ionproject.integration.application.job.JobType import org.ionproject.integration.domain.common.InstitutionModel import org.ionproject.integration.domain.common.ProgrammeModel +import org.ionproject.integration.domain.common.ProgrammeResources import org.ionproject.integration.infrastructure.exception.ArgumentException import org.ionproject.integration.infrastructure.file.INVALID_FORMAT_ERROR import org.ionproject.integration.infrastructure.file.OutputFormat @@ -36,7 +37,7 @@ class InputProcessorTests { institutionModel = testInstitution, name = TEST_PROGRAMME_NAME, acronym = TEST_PROGRAMME_ACRONYM, - timetableUri = TEST_URI + resources = ProgrammeResources(TEST_URI, TEST_URI) ) private val mockInstitutionRepoOK = mock { diff --git a/src/test/kotlin/org/ionproject/integration/utils/DateUtilsTests.kt b/src/test/kotlin/org/ionproject/integration/utils/DateUtilsTests.kt index 555ac414..72246587 100644 --- a/src/test/kotlin/org/ionproject/integration/utils/DateUtilsTests.kt +++ b/src/test/kotlin/org/ionproject/integration/utils/DateUtilsTests.kt @@ -170,8 +170,8 @@ class DateUtilsTests { val intervalDate = DateUtils.getEvaluationDateTimeFrom("2021", dayMonthStr, timeStr, durationStr) // Assert - assertEquals("2021-06-29T19:00", intervalDate.from.toString()) - assertEquals("2021-06-29T22:00", intervalDate.to.toString()) + assertEquals("2021-06-29T19:00Z[UTC]", intervalDate.from.toString()) + assertEquals("2021-06-29T22:00Z[UTC]", intervalDate.to.toString()) } @Test @@ -185,7 +185,7 @@ class DateUtilsTests { val intervalDate = DateUtils.getEvaluationDateTimeFrom("2021", dayMonthStr, timeStr, durationStr) // Assert - assertEquals("2021-06-02T19:30", intervalDate.from.toString()) - assertEquals("2021-06-02T22:00", intervalDate.to.toString()) + assertEquals("2021-06-02T19:30Z[UTC]", intervalDate.from.toString()) + assertEquals("2021-06-02T22:00Z[UTC]", intervalDate.to.toString()) } } From 952871b4720c8ebbee47b199a12e435748548295 Mon Sep 17 00:00:00 2001 From: Grimord Date: Tue, 6 Jul 2021 20:11:31 +0100 Subject: [PATCH 61/67] add timeout to Downloader --- .../job/tasklet/DownloadAndCompareTasklet.kt | 2 +- .../infrastructure/http/DownloaderImpl.kt | 43 ++++++++++++++----- .../infrastructure/http/IFileDownloader.kt | 2 +- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/application/job/tasklet/DownloadAndCompareTasklet.kt b/src/main/kotlin/org/ionproject/integration/application/job/tasklet/DownloadAndCompareTasklet.kt index 487b1ccf..304d5f63 100644 --- a/src/main/kotlin/org/ionproject/integration/application/job/tasklet/DownloadAndCompareTasklet.kt +++ b/src/main/kotlin/org/ionproject/integration/application/job/tasklet/DownloadAndCompareTasklet.kt @@ -63,7 +63,7 @@ class DownloadAndCompareTasklet( throw DownloadAndCompareTaskletException("File already exists in $localFileDestination") } - val path = downloader.download(targetUri, localFileDestination) + val path = downloader.download(targetUri, localFileDestination, appProperties.timeoutInSeconds) .match( { it }, { diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/http/DownloaderImpl.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/http/DownloaderImpl.kt index 83c85bbf..d5c0b213 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/http/DownloaderImpl.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/http/DownloaderImpl.kt @@ -2,19 +2,42 @@ package org.ionproject.integration.infrastructure.http import org.ionproject.integration.infrastructure.Try import org.springframework.stereotype.Service +import java.io.File +import java.lang.IllegalStateException +import java.net.HttpURLConnection import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse import java.nio.file.Path +import java.time.Duration @Service -class DownloaderImpl(val channelProvider: ChannelProvider) : IFileDownloader { - override fun download(uri: URI, localDestination: Path): Try { - channelProvider.getRemoteChannel(uri).use { remoteChannel -> - channelProvider.getLocalChannel(localDestination).use { fileChannel -> - return Try.of { - fileChannel.transferFrom(remoteChannel, 0, Long.MAX_VALUE) - localDestination - } - } - } +class DownloaderImpl : IFileDownloader { + override fun download(uri: URI, localDestination: Path, timeoutInSeconds: Int): Try { + val response = sendRequest(uri, timeoutInSeconds) + + if (response.isError()) + throw IllegalStateException("Server responded with error code ${response.statusCode()}") + + val file = response.writeToFile(localDestination) + return Try.of { file.toPath() } + } + + private fun sendRequest(uri: URI, timeoutInSeconds: Int): HttpResponse { + val client = HttpClient.newHttpClient() + + val request = HttpRequest.newBuilder() + .uri(uri) + .timeout(Duration.ofSeconds(timeoutInSeconds.toLong())) + .build() + + return client.send(request, HttpResponse.BodyHandlers.ofByteArray()) } + + private fun HttpResponse.isError(): Boolean = + statusCode() in HttpURLConnection.HTTP_INTERNAL_ERROR..HttpURLConnection.HTTP_VERSION + + private fun HttpResponse.writeToFile(localDestination: Path): File = + localDestination.toFile().also { file -> file.writeBytes(body()) } } diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/http/IFileDownloader.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/http/IFileDownloader.kt index 3cd23b4e..815fcac1 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/http/IFileDownloader.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/http/IFileDownloader.kt @@ -5,5 +5,5 @@ import java.nio.file.Path import org.ionproject.integration.infrastructure.Try interface IFileDownloader { - fun download(uri: URI, localDestination: Path): Try + fun download(uri: URI, localDestination: Path, timeoutInSeconds: Int = 30): Try } From 1c101aa50e3e4568caf4c949b601fd53a9d33df6 Mon Sep 17 00:00:00 2001 From: Grimord Date: Tue, 6 Jul 2021 20:14:57 +0100 Subject: [PATCH 62/67] add timeout to Downloader --- .../integration/infrastructure/http/DownloaderImpl.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/http/DownloaderImpl.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/http/DownloaderImpl.kt index d5c0b213..e1b8b6fd 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/http/DownloaderImpl.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/http/DownloaderImpl.kt @@ -14,14 +14,14 @@ import java.time.Duration @Service class DownloaderImpl : IFileDownloader { - override fun download(uri: URI, localDestination: Path, timeoutInSeconds: Int): Try { + override fun download(uri: URI, localDestination: Path, timeoutInSeconds: Int): Try = Try.of { val response = sendRequest(uri, timeoutInSeconds) if (response.isError()) throw IllegalStateException("Server responded with error code ${response.statusCode()}") val file = response.writeToFile(localDestination) - return Try.of { file.toPath() } + file.toPath() } private fun sendRequest(uri: URI, timeoutInSeconds: Int): HttpResponse { From 409b8ed4cd91bc00b8986440c665daef10731996 Mon Sep 17 00:00:00 2001 From: Grimord Date: Tue, 6 Jul 2021 20:16:34 +0100 Subject: [PATCH 63/67] add timeout to Downloader --- .../integration/infrastructure/http/DownloaderImpl.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/http/DownloaderImpl.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/http/DownloaderImpl.kt index e1b8b6fd..73abb781 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/http/DownloaderImpl.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/http/DownloaderImpl.kt @@ -14,6 +14,8 @@ import java.time.Duration @Service class DownloaderImpl : IFileDownloader { + private val client by lazy { HttpClient.newHttpClient() } + override fun download(uri: URI, localDestination: Path, timeoutInSeconds: Int): Try = Try.of { val response = sendRequest(uri, timeoutInSeconds) @@ -25,8 +27,6 @@ class DownloaderImpl : IFileDownloader { } private fun sendRequest(uri: URI, timeoutInSeconds: Int): HttpResponse { - val client = HttpClient.newHttpClient() - val request = HttpRequest.newBuilder() .uri(uri) .timeout(Duration.ofSeconds(timeoutInSeconds.toLong())) From dd6d273e6db5492f8f07fabf6cfb043f36f99d7a Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Thu, 8 Jul 2021 01:47:28 +0100 Subject: [PATCH 64/67] Creation of DTOs and respective tests. --- .../integration/application/dto/ParsedData.kt | 54 +++- .../application/job/ISELEvaluationsJob.kt | 30 ++- .../domain/common/dto/ProgrammeDto.kt | 6 + .../evaluations/OutputRepresentations.kt | 23 +- .../timetable/dto/OutputRepresentations.kt | 8 +- .../integration/dispatcher/FileWriterTests.kt | 6 +- ...EvaluationsBusinessObjFormatCheckerTest.kt | 10 +- .../EvaluationsDtoFormatCheckerTest.kt | 246 ++++++++++++++++++ .../TimetableDtoFormatCheckerTest.kt | 2 +- .../implementations/WriteFileTaskletTests.kt | 2 +- 10 files changed, 357 insertions(+), 30 deletions(-) create mode 100644 src/main/kotlin/org/ionproject/integration/domain/common/dto/ProgrammeDto.kt create mode 100644 src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsDtoFormatCheckerTest.kt diff --git a/src/main/kotlin/org/ionproject/integration/application/dto/ParsedData.kt b/src/main/kotlin/org/ionproject/integration/application/dto/ParsedData.kt index f20162c1..a4d3b214 100644 --- a/src/main/kotlin/org/ionproject/integration/application/dto/ParsedData.kt +++ b/src/main/kotlin/org/ionproject/integration/application/dto/ParsedData.kt @@ -1,8 +1,10 @@ package org.ionproject.integration.application.dto +import org.ionproject.integration.domain.calendar.AcademicCalendarDto +import org.ionproject.integration.domain.common.Term +import org.ionproject.integration.domain.evaluations.EvaluationsDto import org.ionproject.integration.domain.timetable.dto.TimetableDto import org.ionproject.integration.infrastructure.file.Filepath -import org.ionproject.integration.domain.calendar.AcademicCalendarDto /** * ParsedData will be used to "transport" the final data along with the required metadata. @@ -74,4 +76,54 @@ data class AcademicCalendarData( return staging + segments } + + data class EvaluationsData( + val programme: ProgrammeMetadata, + val term: CalendarTerm, + private val dto: EvaluationsDto + ) : ParsedData(dto) { + companion object Factory { + private const val ACADEMIC_YEAR_LENGTH = 9 + private const val ACADEMIC_YEARS_FOLDER_NAME = "academic_years" + + fun from(evaluationsDto: EvaluationsDto, identifier: String): EvaluationsData = + EvaluationsData( + ProgrammeMetadata( + InstitutionMetadata( + evaluationsDto.school.name, + evaluationsDto.school.acr, + identifier + ), + evaluationsDto.programme.name, + evaluationsDto.programme.acr + ), + CalendarTerm( + evaluationsDto.calendarTerm.take(4).toInt(), + when (evaluationsDto.calendarTerm.takeLast(1).toInt()) { + 1 -> Term.FALL + 2 -> Term.SPRING + else -> throw IllegalArgumentException("Invalid Term ${evaluationsDto.calendarTerm}") + } + ), + evaluationsDto + ) + } + + private val PROGRAMMES = "programmes" + + override val identifier: String + get() = "${javaClass.simpleName}:${programme.acronym}:$term" + + override fun getDirectory(repositoryName: String, staging: Filepath): Filepath { + val segments = listOf( + repositoryName, + programme.institution.domain, + PROGRAMMES, + programme.acronym, + term.toString() + ) + + return staging + segments + } + } } diff --git a/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt b/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt index 132ea39e..804282a4 100644 --- a/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt +++ b/src/main/kotlin/org/ionproject/integration/application/job/ISELEvaluationsJob.kt @@ -2,7 +2,9 @@ package org.ionproject.integration.application.job import org.ionproject.integration.application.JobEngine import org.ionproject.integration.application.config.AppProperties +import org.ionproject.integration.application.dispatcher.DispatchResult import org.ionproject.integration.application.dispatcher.IDispatcher +import org.ionproject.integration.application.dto.AcademicCalendarData import org.ionproject.integration.application.job.tasklet.DownloadAndCompareTasklet import org.ionproject.integration.domain.common.InstitutionModel import org.ionproject.integration.domain.common.ProgrammeModel @@ -12,6 +14,7 @@ import org.ionproject.integration.domain.evaluations.RawEvaluationsData import org.ionproject.integration.infrastructure.Try import org.ionproject.integration.infrastructure.file.FileComparatorImpl import org.ionproject.integration.infrastructure.file.FileDigestImpl +import org.ionproject.integration.infrastructure.file.OutputFormat import org.ionproject.integration.infrastructure.hash.HashRepositoryImpl import org.ionproject.integration.infrastructure.http.IFileDownloader import org.ionproject.integration.infrastructure.orThrow @@ -20,6 +23,7 @@ import org.ionproject.integration.infrastructure.pdfextractor.ITextPdfExtractor import org.ionproject.integration.infrastructure.pdfextractor.PDFBytesFormatChecker import org.ionproject.integration.infrastructure.repository.IInstitutionRepository import org.ionproject.integration.infrastructure.repository.IProgrammeRepository +import org.springframework.batch.core.ExitStatus import org.springframework.batch.core.configuration.annotation.JobBuilderFactory import org.springframework.batch.core.configuration.annotation.StepBuilderFactory import org.springframework.batch.core.configuration.annotation.StepScope @@ -55,8 +59,8 @@ class ISELEvaluationsJob( .on("STOPPED").end() .next(extractEvaluationsPDFTasklet()) .next(createEvaluationsPDFBusinessObjectsTasklet()) - // .next(createEvaluationsPDFDtoTasklet()) - // .next(writeEvaluationsDTOToGitTasklet()) + .next(createEvaluationsDtoTasklet()) + .next(writeEvaluationsDTOToGitTasklet()) .build().listener(NotificationListener()) .build() @@ -128,22 +132,26 @@ class ISELEvaluationsJob( ) @Bean - fun createEvaluationsPDFDtoTasklet() = stepBuilderFactory.get("Create DTO from Evaluations Business Objects") + fun createEvaluationsDtoTasklet() = stepBuilderFactory.get("Create DTO from Evaluations Business Objects") .tasklet { _, _ -> State.evaluationsDto = EvaluationsDto.from(State.evaluations) RepeatStatus.FINISHED } .build() - // TODO @Bean - fun writeEvaluationsDTOToGitTasklet() = stepBuilderFactory.get("Write Calendar DTO to Git") - .tasklet { _, _ -> -/* dispatcher.dispatch( - EvaluationsData.from(State.evaluationsDto), - EVALUATIONS_JOB_NAME, - OutputFormat.JSON - )*/ + fun writeEvaluationsDTOToGitTasklet() = stepBuilderFactory.get("Write Evaluations DTO to Git") + .tasklet { stepContribution, context -> + val formatParam = context.stepContext.jobParameters[JobEngine.FORMAT_PARAMETER] as String + val identifier = context.stepContext.jobParameters[JobEngine.INSTITUTION_PARAMETER] as String + val format = OutputFormat.of(formatParam) + val evaluationsData = AcademicCalendarData.EvaluationsData.from(ISELEvaluationsJob.State.evaluationsDto, identifier) + + val dispatchResult = dispatcher.dispatch(evaluationsData, EVALUATIONS_JOB_NAME, format) + + if (dispatchResult == DispatchResult.FAILURE) { + stepContribution.exitStatus = ExitStatus.FAILED + } RepeatStatus.FINISHED } .build() diff --git a/src/main/kotlin/org/ionproject/integration/domain/common/dto/ProgrammeDto.kt b/src/main/kotlin/org/ionproject/integration/domain/common/dto/ProgrammeDto.kt new file mode 100644 index 00000000..c7b82e46 --- /dev/null +++ b/src/main/kotlin/org/ionproject/integration/domain/common/dto/ProgrammeDto.kt @@ -0,0 +1,6 @@ +package org.ionproject.integration.domain.common.dto + +data class ProgrammeDto( + val name: String, + val acr: String, +) diff --git a/src/main/kotlin/org/ionproject/integration/domain/evaluations/OutputRepresentations.kt b/src/main/kotlin/org/ionproject/integration/domain/evaluations/OutputRepresentations.kt index c7aaecd2..1ebd991c 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/evaluations/OutputRepresentations.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/evaluations/OutputRepresentations.kt @@ -1,11 +1,14 @@ package org.ionproject.integration.domain.evaluations +import org.ionproject.integration.domain.common.dto.ProgrammeDto import org.ionproject.integration.domain.common.dto.SchoolDto +import org.ionproject.integration.infrastructure.DateUtils data class EvaluationsDto( val creationDateTime: String = "", val retrievalDateTime: String = "", val school: SchoolDto, + val programme: ProgrammeDto, val calendarTerm: String, val exams: List ) { @@ -18,8 +21,12 @@ data class EvaluationsDto( evaluations.school.name, evaluations.school.acr ), + ProgrammeDto( + evaluations.programme.name, + evaluations.programme.acr + ), evaluations.calendarTerm, - emptyList() + ExamDto.from(evaluations.exams) ) } } @@ -31,4 +38,16 @@ data class ExamDto( val endDate: String, val category: String, val location: String -) +) { + companion object { + fun from(exams: List): List = exams.map { + ExamDto( + it.course, + DateUtils.formatToISO8601(it.startDate), + DateUtils.formatToISO8601(it.endDate), + it.category.name, + it.location + ) + } + } +} diff --git a/src/main/kotlin/org/ionproject/integration/domain/timetable/dto/OutputRepresentations.kt b/src/main/kotlin/org/ionproject/integration/domain/timetable/dto/OutputRepresentations.kt index e0622720..5cb245ae 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/timetable/dto/OutputRepresentations.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/timetable/dto/OutputRepresentations.kt @@ -1,13 +1,14 @@ package org.ionproject.integration.domain.timetable.dto import com.fasterxml.jackson.annotation.JsonInclude +import org.ionproject.integration.domain.common.dto.ProgrammeDto import org.ionproject.integration.domain.common.dto.SchoolDto +import org.ionproject.integration.domain.timetable.TimetableTeachers import org.ionproject.integration.domain.timetable.model.Course import org.ionproject.integration.domain.timetable.model.CourseTeacher import org.ionproject.integration.domain.timetable.model.EventCategory import org.ionproject.integration.domain.timetable.model.Instructor import org.ionproject.integration.domain.timetable.model.RecurrentEvent -import org.ionproject.integration.domain.timetable.TimetableTeachers data class TimetableDto( val creationDateTime: String, @@ -84,11 +85,6 @@ data class TimetableDto( } } -data class ProgrammeDto( - val name: String, - val acr: String, -) - data class ClassDto( val acr: String, val sections: List diff --git a/src/test/kotlin/org/ionproject/integration/dispatcher/FileWriterTests.kt b/src/test/kotlin/org/ionproject/integration/dispatcher/FileWriterTests.kt index 48449ab8..c4c8c027 100644 --- a/src/test/kotlin/org/ionproject/integration/dispatcher/FileWriterTests.kt +++ b/src/test/kotlin/org/ionproject/integration/dispatcher/FileWriterTests.kt @@ -6,15 +6,15 @@ import org.ionproject.integration.application.dto.InstitutionMetadata import org.ionproject.integration.application.dto.ProgrammeMetadata import org.ionproject.integration.application.dto.TimetableData import org.ionproject.integration.domain.common.Term +import org.ionproject.integration.domain.common.dto.ProgrammeDto +import org.ionproject.integration.domain.common.dto.SchoolDto import org.ionproject.integration.domain.timetable.dto.ClassDto import org.ionproject.integration.domain.timetable.dto.EventDto import org.ionproject.integration.domain.timetable.dto.InstructorDto -import org.ionproject.integration.domain.timetable.dto.ProgrammeDto -import org.ionproject.integration.domain.common.dto.SchoolDto import org.ionproject.integration.domain.timetable.dto.SectionDto import org.ionproject.integration.domain.timetable.dto.TimetableDto -import org.ionproject.integration.infrastructure.file.OutputFormat import org.ionproject.integration.infrastructure.file.FileWriter +import org.ionproject.integration.infrastructure.file.OutputFormat import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired diff --git a/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt b/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt index 6f644003..b4be6b1d 100644 --- a/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt +++ b/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt @@ -76,12 +76,12 @@ class EvaluationsBusinessObjFormatCheckerTest { ) ) - val academicCalendarRetrieved = Evaluations.from(evaluationsData, programme) + val evaluationsRetrieved = Evaluations.from(evaluationsData, programme) - val academicCalendarExpected = + val evaluationsExpected = Evaluations( - academicCalendarRetrieved.creationDateTime, // 2020-2021 Evaluations PDF doesn't have a creation date in its properties, so it gets the retrieval date time. - academicCalendarRetrieved.retrievalDateTime, + evaluationsRetrieved.creationDateTime, // 2020-2021 Evaluations PDF doesn't have a creation date in its properties, so it gets the retrieval date time. + evaluationsRetrieved.retrievalDateTime, School( "Instituto Superior de Engenharia de Lisboa", "ISEL" @@ -781,6 +781,6 @@ class EvaluationsBusinessObjFormatCheckerTest { ) ) - assertEquals(academicCalendarExpected, academicCalendarRetrieved) + assertEquals(evaluationsExpected, evaluationsRetrieved) } } diff --git a/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsDtoFormatCheckerTest.kt b/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsDtoFormatCheckerTest.kt new file mode 100644 index 00000000..8561b991 --- /dev/null +++ b/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsDtoFormatCheckerTest.kt @@ -0,0 +1,246 @@ +package org.ionproject.integration.format.implementations + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import org.ionproject.integration.domain.calendar.AcademicCalendarDto +import org.ionproject.integration.domain.calendar.EvaluationDto +import org.ionproject.integration.domain.calendar.EventDto +import org.ionproject.integration.domain.calendar.TermDto +import org.ionproject.integration.domain.common.Language +import org.ionproject.integration.domain.common.School +import org.ionproject.integration.domain.common.dto.ProgrammeDto +import org.ionproject.integration.domain.common.dto.SchoolDto +import org.ionproject.integration.domain.evaluations.EvaluationsDto +import org.ionproject.integration.domain.evaluations.ExamDto +import org.ionproject.integration.model.external.calendar.AcademicCalendar +import org.ionproject.integration.model.external.calendar.Evaluation +import org.ionproject.integration.model.external.calendar.Event +import org.ionproject.integration.model.external.calendar.Term +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.Month + +internal class EvaluationsDtoFormatCheckerTest { + + private val mapper = jacksonObjectMapper() + + @Test + fun `when Serialized Evaluations is equal to expected Dto then Success`() { + + val evaluations = EvaluationsDto( + "2021-07-08T00:12:56Z", + "2021-07-08T00:12:56Z", + SchoolDto("Instituto Superior Engenharia Lisboa", "ISEL"), + ProgrammeDto("Mestrado em Engenharia Informática e de Computadores", "MEIC"), + "2020-2021-2", + listOf( + ExamDto( + "AMD", + "2021-09-13T19:00:00Z", + "2021-09-13T19:00:00Z", + "EXAM_SPECIAL", + "" + ), + ExamDto( + "ASI", + "2021-09-15T19:00:00Z", + "2021-09-15T22:00:00Z", + "EXAM_SPECIAL", + "" + ), + ExamDto( + "AMD", + "2021-09-13T19:00:00Z", + "2021-09-13T19:00:00Z", + "EXAM_SPECIAL", + "" + ), + ExamDto( + "AMD", + "2021-09-13T19:00:00Z", + "2021-09-13T19:00:00Z", + "EXAM_SPECIAL", + "" + ), + ExamDto( + "AMD", + "2021-09-13T19:00:00Z", + "2021-09-13T19:00:00Z", + "EXAM_SPECIAL", + "" + ), + ExamDto( + "AMD", + "2021-09-13T19:00:00Z", + "2021-09-13T19:00:00Z", + "EXAM_SPECIAL", + "" + ) + ) + ) + val serialized = mapper.writeValueAsString(evaluations) + + val json = + """{"creationDateTime":"2021-07-08T00:12:56Z","retrievalDateTime":"2021-07-08T00:12:56Z","school":{"name":"Instituto Superior de Engenharia de Lisboa","acr":"ISEL"},"programme":{"name":"Mestrado em Engenharia Informática e de Computadores","acr":"MEIC"},"calendarTerm":"2020-2021-2","exams":[{"course":"AMD","startDate":"2021-09-13T19:00:00Z","endDate":"2021-09-13T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"ASI","startDate":"2021-09-15T19:00:00Z","endDate":"2021-09-15T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"CCD","startDate":"2021-09-17T19:00:00Z","endDate":"2021-09-17T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"CD","startDate":"2021-09-15T19:00:00Z","endDate":"2021-09-15T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"CDLE","startDate":"2021-09-06T19:00:00Z","endDate":"2021-09-06T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"CS","startDate":"2021-09-06T19:00:00Z","endDate":"2021-09-06T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"CSI","startDate":"2021-09-10T19:00:00Z","endDate":"2021-09-10T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"DI3D","startDate":"2021-09-14T19:00:00Z","endDate":"2021-09-14T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"EGP","startDate":"2021-07-01T19:00:00Z","endDate":"2021-07-01T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"EGP","startDate":"2021-07-20T14:00:00Z","endDate":"2021-07-20T17:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"EGP","startDate":"2021-09-09T19:00:00Z","endDate":"2021-09-09T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"ES","startDate":"2021-09-04T10:00:00Z","endDate":"2021-09-04T13:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"GSI","startDate":"2021-07-07T14:00:00Z","endDate":"2021-07-07T17:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"GSI","startDate":"2021-07-22T19:00:00Z","endDate":"2021-07-22T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"GSI","startDate":"2021-09-02T19:00:00Z","endDate":"2021-09-02T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"IESD","startDate":"2021-06-29T19:00:00Z","endDate":"2021-06-29T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"IESD","startDate":"2021-07-20T10:00:00Z","endDate":"2021-07-20T13:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"IESD","startDate":"2021-09-14T19:00:00Z","endDate":"2021-09-14T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"IoT","startDate":"2021-07-15T10:00:00Z","endDate":"2021-07-15T13:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"IoT","startDate":"2021-07-28T19:00:00Z","endDate":"2021-07-28T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"IoT","startDate":"2021-09-03T14:00:00Z","endDate":"2021-09-03T17:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"IRS","startDate":"2021-09-08T19:00:00Z","endDate":"2021-09-08T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"MDLE","startDate":"2021-07-13T14:00:00Z","endDate":"2021-07-13T17:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"MDLE","startDate":"2021-07-19T14:00:00Z","endDate":"2021-07-19T17:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"MDLE","startDate":"2021-09-07T14:00:00Z","endDate":"2021-09-07T17:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"PIB","startDate":"2021-07-16T19:00:00Z","endDate":"2021-07-16T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"PIB","startDate":"2021-07-26T10:00:00Z","endDate":"2021-07-26T13:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"PIB","startDate":"2021-09-15T19:00:00Z","endDate":"2021-09-15T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"RCMov","startDate":"2021-07-12T19:00:00Z","endDate":"2021-07-12T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"RCMov","startDate":"2021-07-27T10:00:00Z","endDate":"2021-07-27T13:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"RCMov","startDate":"2021-09-11T10:00:00Z","endDate":"2021-09-11T13:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"RDC","startDate":"2021-09-08T19:00:00Z","endDate":"2021-09-08T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"RPC","startDate":"2021-07-01T19:00:00Z","endDate":"2021-07-01T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"RPC","startDate":"2021-07-29T19:00:00Z","endDate":"2021-07-29T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"RPC","startDate":"2021-09-17T14:00:00Z","endDate":"2021-09-17T17:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"SIAD","startDate":"2021-07-05T19:00:00Z","endDate":"2021-07-05T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"SIAD","startDate":"2021-07-21T14:00:00Z","endDate":"2021-07-21T17:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"SIAD","startDate":"2021-09-08T19:00:00Z","endDate":"2021-09-08T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"SRC","startDate":"2021-07-09T10:00:00Z","endDate":"2021-07-09T13:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"SRC","startDate":"2021-07-23T19:00:00Z","endDate":"2021-07-23T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"SRC","startDate":"2021-09-13T10:00:00Z","endDate":"2021-09-13T13:00:00Z","category":"EXAM_SPECIAL","location":""}]}""" + + assertEquals(json, serialized) + } + + @Test + fun `when Business object is equal to expected Dto then Success`() { + val academicCalendarDto = AcademicCalendarDto( + "20200706T160021Z", + "20210606T235001Z", + SchoolDto("Instituto Superior Engenharia Lisboa", "ISEL"), + "pt-PT", + listOf( + TermDto( + "2020-2021-1", + listOf( + EventDto( + "Interrupção de atividades letivas (Natal)", + "2020-12-21", + "2021-01-03" + ) + ), + listOf( + EvaluationDto( + "Período de exames (época normal)", + "2021-01-25", + "2021-02-13", + false + ) + ), + listOf(), + listOf( + EventDto( + "Divulgação de horários", + "2020-09-09", + "2020-09-09" + ), + EventDto( + "Abertura das atividades letivas 2020/2021", + "2020-09-28", + "2020-09-28" + ) + ) + ), + TermDto( + "2020-2021-2", + listOf( + EventDto( + "Interrupção de atividades letivas (Páscoa)", + "2021-03-29", + "2021-04-05" + ) + ), + listOf( + EvaluationDto( + "Período de exames (época normal)", + "2021-06-28", + "2021-07-17", + false + ), + EvaluationDto( + "Período de exames (época de recurso)", + "2021-07-19", + "2021-07-31", + false + ) + ), + listOf(), + listOf( + EventDto( + "Divulgação de horários", + "2021-02-22", + "2021-02-22" + ), + EventDto( + "Início das aulas", + "2021-03-15", + "2021-03-15" + ) + ) + ) + ) + ) + + val academicCalendarBO = AcademicCalendar( + "20200706T160021Z", + "20210606T235001Z", + School("Instituto Superior Engenharia Lisboa", "ISEL"), + Language.PT, + listOf( + Term( + "2020-2021-1", + listOf( + Event( + "Interrupção de atividades letivas (Natal)", + LocalDate.of(2020, Month.DECEMBER, 21), + LocalDate.of(2021, Month.JANUARY, 3) + ) + ), + listOf( + Evaluation( + "Período de exames (época normal)", + LocalDate.of(2021, Month.JANUARY, 25), + LocalDate.of(2021, Month.FEBRUARY, 13), + false + ) + ), + listOf(), + listOf( + Event( + "Divulgação de horários", + LocalDate.of(2020, Month.SEPTEMBER, 9), + LocalDate.of(2020, Month.SEPTEMBER, 9) + ), + Event( + "Abertura das atividades letivas 2020/2021", + LocalDate.of(2020, Month.SEPTEMBER, 28), + LocalDate.of(2020, Month.SEPTEMBER, 28) + ) + ) + ), + Term( + "2020-2021-2", + listOf( + Event( + "Interrupção de atividades letivas (Páscoa)", + LocalDate.of(2021, Month.MARCH, 29), + LocalDate.of(2021, Month.APRIL, 5) + ) + ), + listOf( + Evaluation( + "Período de exames (época normal)", + LocalDate.of(2021, Month.JUNE, 28), + LocalDate.of(2021, Month.JULY, 17), + false + ), + Evaluation( + "Período de exames (época de recurso)", + LocalDate.of(2021, Month.JULY, 19), + LocalDate.of(2021, Month.JULY, 31), + false + ) + ), + listOf(), + listOf( + Event( + "Divulgação de horários", + LocalDate.of(2021, Month.FEBRUARY, 22), + LocalDate.of(2021, Month.FEBRUARY, 22) + ), + Event( + "Início das aulas", + LocalDate.of(2021, Month.MARCH, 15), + LocalDate.of(2021, Month.MARCH, 15) + ) + ) + ) + ) + ) + assertEquals(AcademicCalendarDto.from(academicCalendarBO), academicCalendarDto) + } +} diff --git a/src/test/kotlin/org/ionproject/integration/format/implementations/TimetableDtoFormatCheckerTest.kt b/src/test/kotlin/org/ionproject/integration/format/implementations/TimetableDtoFormatCheckerTest.kt index 90bc5527..85c4ee0d 100644 --- a/src/test/kotlin/org/ionproject/integration/format/implementations/TimetableDtoFormatCheckerTest.kt +++ b/src/test/kotlin/org/ionproject/integration/format/implementations/TimetableDtoFormatCheckerTest.kt @@ -12,7 +12,7 @@ import org.ionproject.integration.domain.timetable.model.Instructor import org.ionproject.integration.domain.timetable.dto.InstructorDto import org.ionproject.integration.domain.timetable.model.Label import org.ionproject.integration.domain.common.Programme -import org.ionproject.integration.domain.timetable.dto.ProgrammeDto +import org.ionproject.integration.domain.common.dto.ProgrammeDto import org.ionproject.integration.domain.timetable.model.RecurrentEvent import org.ionproject.integration.domain.common.School import org.ionproject.integration.domain.common.dto.SchoolDto diff --git a/src/test/kotlin/org/ionproject/integration/step/tasklet/iseltimetable/implementations/WriteFileTaskletTests.kt b/src/test/kotlin/org/ionproject/integration/step/tasklet/iseltimetable/implementations/WriteFileTaskletTests.kt index 57811f2a..436836a4 100644 --- a/src/test/kotlin/org/ionproject/integration/step/tasklet/iseltimetable/implementations/WriteFileTaskletTests.kt +++ b/src/test/kotlin/org/ionproject/integration/step/tasklet/iseltimetable/implementations/WriteFileTaskletTests.kt @@ -5,7 +5,7 @@ import org.ionproject.integration.application.dispatcher.DispatchResult import org.ionproject.integration.application.dispatcher.IDispatcher import org.ionproject.integration.application.job.ISELTimetableJob import org.ionproject.integration.domain.common.Programme -import org.ionproject.integration.domain.timetable.dto.ProgrammeDto +import org.ionproject.integration.domain.common.dto.ProgrammeDto import org.ionproject.integration.domain.common.School import org.ionproject.integration.domain.common.dto.SchoolDto import org.ionproject.integration.domain.timetable.Timetable From f658e6fae6d89f6432250499bcdaa5bccaa7fce2 Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Fri, 9 Jul 2021 00:59:33 +0100 Subject: [PATCH 65/67] Completion of DTO tests and removal of any substring with hyphen in the course acronym. --- .../domain/evaluations/BusinessObjects.kt | 15 +- .../pdfextractor/ITextPdfExtractor.kt | 20 +- ...EvaluationsBusinessObjFormatCheckerTest.kt | 6 +- .../EvaluationsDtoFormatCheckerTest.kt | 932 +++++++++++++----- 4 files changed, 732 insertions(+), 241 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt index 331a1e62..00485a3f 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt @@ -94,7 +94,7 @@ data class Evaluations( ) examList.add( Exam( - cleanedLine[TableColumn.COURSE.ordinal].text, + trimCourse(cleanedLine[TableColumn.COURSE.ordinal].text), intervalDateTimeNormal.from, intervalDateTimeNormal.to, ExamCategory.EXAM_NORMAL, @@ -110,7 +110,7 @@ data class Evaluations( ) examList.add( Exam( - cleanedLine[TableColumn.COURSE.ordinal].text, + trimCourse(cleanedLine[TableColumn.COURSE.ordinal].text), intervalDateTimeAltern.from, intervalDateTimeAltern.to, ExamCategory.EXAM_ALTERN, @@ -126,7 +126,7 @@ data class Evaluations( ) examList.add( Exam( - cleanedLine[TableColumn.COURSE.ordinal].text, + trimCourse(cleanedLine[TableColumn.COURSE.ordinal].text), intervalDateTimeSpecial.from, intervalDateTimeSpecial.to, ExamCategory.EXAM_SPECIAL, @@ -146,7 +146,7 @@ data class Evaluations( ) examList.add( Exam( - cleanedLine[TableColumnWinterCourse.COURSE.ordinal].text, + trimCourse(cleanedLine[TableColumn.COURSE.ordinal].text), intervalDateTimeSpecial.from, intervalDateTimeSpecial.to, ExamCategory.EXAM_SPECIAL, @@ -158,6 +158,13 @@ data class Evaluations( } return examList.toList() } + + /** + * Removes any substring from the course acronum that starts with hyphen. + * Examples: SO-leect-leirt, SS-leetc + */ + private fun trimCourse(course: String) = + course.substringBefore("-") } } diff --git a/src/main/kotlin/org/ionproject/integration/infrastructure/pdfextractor/ITextPdfExtractor.kt b/src/main/kotlin/org/ionproject/integration/infrastructure/pdfextractor/ITextPdfExtractor.kt index 0bd9722e..8a73cf76 100644 --- a/src/main/kotlin/org/ionproject/integration/infrastructure/pdfextractor/ITextPdfExtractor.kt +++ b/src/main/kotlin/org/ionproject/integration/infrastructure/pdfextractor/ITextPdfExtractor.kt @@ -1,17 +1,16 @@ package org.ionproject.integration.infrastructure.pdfextractor +import com.itextpdf.kernel.pdf.PdfDate import com.itextpdf.kernel.pdf.PdfDocument +import com.itextpdf.kernel.pdf.PdfName import com.itextpdf.kernel.pdf.PdfReader import com.itextpdf.kernel.pdf.canvas.parser.PdfTextExtractor import org.ionproject.integration.infrastructure.DateUtils import org.ionproject.integration.infrastructure.Try import org.ionproject.integration.infrastructure.exception.PdfExtractorException -import java.nio.file.LinkOption -import java.nio.file.Path -import java.nio.file.attribute.BasicFileAttributes import java.time.ZoneId import java.time.ZonedDateTime -import kotlin.io.path.readAttributes +import java.util.Calendar class ITextPdfExtractor : IPdfExtractor { /** @@ -33,7 +32,7 @@ class ITextPdfExtractor : IPdfExtractor { val pageData = PdfTextExtractor.getTextFromPage(pdfDoc.getPage(i)) data.add(pageData) } - data.add(getCreationDateFromPdfDocument(pdfPath)) + data.add(getCreationDateFromPdfDocument(pdfDoc)) } .map { data.toList() } .mapError { PdfExtractorException("Itext cannot process file") } @@ -43,14 +42,13 @@ class ITextPdfExtractor : IPdfExtractor { } } - private fun getCreationDateFromPdfDocument(pdfPath: String): String { - val creationDate = - Path.of(pdfPath) - .readAttributes(LinkOption.NOFOLLOW_LINKS) - .creationTime() + private fun getCreationDateFromPdfDocument(pdfDocument: PdfDocument): String { + val creationDateString = pdfDocument.documentInfo.getMoreInfo(PdfName.CreationDate.value) + val creationDateCalendar = PdfDate.decode(creationDateString) ?: Calendar.getInstance() + return DateUtils.formatToISO8601( ZonedDateTime.ofInstant( - creationDate.toInstant(), + creationDateCalendar.toInstant(), ZoneId.systemDefault() ) ) diff --git a/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt b/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt index b4be6b1d..5eac144b 100644 --- a/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt +++ b/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsBusinessObjFormatCheckerTest.kt @@ -695,21 +695,21 @@ class EvaluationsBusinessObjFormatCheckerTest { "" ), Exam( - "SO-leic", + "SO", ZonedDateTime.of(2021, 7, 9, 10, 0, 0, 0, ZoneId.systemDefault()), ZonedDateTime.of(2021, 7, 9, 13, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_NORMAL, "" ), Exam( - "SO-leic", + "SO", ZonedDateTime.of(2021, 7, 26, 19, 0, 0, 0, ZoneId.systemDefault()), ZonedDateTime.of(2021, 7, 26, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_ALTERN, "" ), Exam( - "SO-leic", + "SO", ZonedDateTime.of(2021, 9, 7, 19, 0, 0, 0, ZoneId.systemDefault()), ZonedDateTime.of(2021, 9, 7, 22, 0, 0, 0, ZoneId.systemDefault()), ExamCategory.EXAM_SPECIAL, diff --git a/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsDtoFormatCheckerTest.kt b/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsDtoFormatCheckerTest.kt index 8561b991..f81d60c1 100644 --- a/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsDtoFormatCheckerTest.kt +++ b/src/test/kotlin/org/ionproject/integration/format/implementations/EvaluationsDtoFormatCheckerTest.kt @@ -1,24 +1,17 @@ package org.ionproject.integration.format.implementations import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import org.ionproject.integration.domain.calendar.AcademicCalendarDto -import org.ionproject.integration.domain.calendar.EvaluationDto -import org.ionproject.integration.domain.calendar.EventDto -import org.ionproject.integration.domain.calendar.TermDto -import org.ionproject.integration.domain.common.Language +import org.ionproject.integration.domain.common.Programme import org.ionproject.integration.domain.common.School -import org.ionproject.integration.domain.common.dto.ProgrammeDto -import org.ionproject.integration.domain.common.dto.SchoolDto +import org.ionproject.integration.domain.evaluations.Evaluations import org.ionproject.integration.domain.evaluations.EvaluationsDto -import org.ionproject.integration.domain.evaluations.ExamDto -import org.ionproject.integration.model.external.calendar.AcademicCalendar -import org.ionproject.integration.model.external.calendar.Evaluation -import org.ionproject.integration.model.external.calendar.Event -import org.ionproject.integration.model.external.calendar.Term +import org.ionproject.integration.domain.evaluations.Exam +import org.ionproject.integration.domain.evaluations.ExamCategory +import org.ionproject.integration.infrastructure.DateUtils import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test -import java.time.LocalDate -import java.time.Month +import java.time.ZoneId +import java.time.ZonedDateTime internal class EvaluationsDtoFormatCheckerTest { @@ -27,220 +20,713 @@ internal class EvaluationsDtoFormatCheckerTest { @Test fun `when Serialized Evaluations is equal to expected Dto then Success`() { - val evaluations = EvaluationsDto( - "2021-07-08T00:12:56Z", - "2021-07-08T00:12:56Z", - SchoolDto("Instituto Superior Engenharia Lisboa", "ISEL"), - ProgrammeDto("Mestrado em Engenharia Informática e de Computadores", "MEIC"), - "2020-2021-2", - listOf( - ExamDto( - "AMD", - "2021-09-13T19:00:00Z", - "2021-09-13T19:00:00Z", - "EXAM_SPECIAL", - "" + val evaluations = + Evaluations( + DateUtils.formatToISO8601(ZonedDateTime.of(2021, 7, 8, 0, 12, 56, 0, ZoneId.systemDefault())), + DateUtils.formatToISO8601(ZonedDateTime.of(2021, 7, 8, 0, 12, 56, 0, ZoneId.systemDefault())), + School( + "Instituto Superior de Engenharia de Lisboa", + "ISEL" ), - ExamDto( - "ASI", - "2021-09-15T19:00:00Z", - "2021-09-15T22:00:00Z", - "EXAM_SPECIAL", - "" + Programme( + "Licenciatura em Engenharia Informática e de Computadores", + "LEIC" ), - ExamDto( - "AMD", - "2021-09-13T19:00:00Z", - "2021-09-13T19:00:00Z", - "EXAM_SPECIAL", - "" - ), - ExamDto( - "AMD", - "2021-09-13T19:00:00Z", - "2021-09-13T19:00:00Z", - "EXAM_SPECIAL", - "" - ), - ExamDto( - "AMD", - "2021-09-13T19:00:00Z", - "2021-09-13T19:00:00Z", - "EXAM_SPECIAL", - "" - ), - ExamDto( - "AMD", - "2021-09-13T19:00:00Z", - "2021-09-13T19:00:00Z", - "EXAM_SPECIAL", - "" - ) - ) - ) - val serialized = mapper.writeValueAsString(evaluations) - - val json = - """{"creationDateTime":"2021-07-08T00:12:56Z","retrievalDateTime":"2021-07-08T00:12:56Z","school":{"name":"Instituto Superior de Engenharia de Lisboa","acr":"ISEL"},"programme":{"name":"Mestrado em Engenharia Informática e de Computadores","acr":"MEIC"},"calendarTerm":"2020-2021-2","exams":[{"course":"AMD","startDate":"2021-09-13T19:00:00Z","endDate":"2021-09-13T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"ASI","startDate":"2021-09-15T19:00:00Z","endDate":"2021-09-15T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"CCD","startDate":"2021-09-17T19:00:00Z","endDate":"2021-09-17T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"CD","startDate":"2021-09-15T19:00:00Z","endDate":"2021-09-15T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"CDLE","startDate":"2021-09-06T19:00:00Z","endDate":"2021-09-06T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"CS","startDate":"2021-09-06T19:00:00Z","endDate":"2021-09-06T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"CSI","startDate":"2021-09-10T19:00:00Z","endDate":"2021-09-10T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"DI3D","startDate":"2021-09-14T19:00:00Z","endDate":"2021-09-14T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"EGP","startDate":"2021-07-01T19:00:00Z","endDate":"2021-07-01T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"EGP","startDate":"2021-07-20T14:00:00Z","endDate":"2021-07-20T17:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"EGP","startDate":"2021-09-09T19:00:00Z","endDate":"2021-09-09T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"ES","startDate":"2021-09-04T10:00:00Z","endDate":"2021-09-04T13:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"GSI","startDate":"2021-07-07T14:00:00Z","endDate":"2021-07-07T17:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"GSI","startDate":"2021-07-22T19:00:00Z","endDate":"2021-07-22T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"GSI","startDate":"2021-09-02T19:00:00Z","endDate":"2021-09-02T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"IESD","startDate":"2021-06-29T19:00:00Z","endDate":"2021-06-29T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"IESD","startDate":"2021-07-20T10:00:00Z","endDate":"2021-07-20T13:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"IESD","startDate":"2021-09-14T19:00:00Z","endDate":"2021-09-14T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"IoT","startDate":"2021-07-15T10:00:00Z","endDate":"2021-07-15T13:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"IoT","startDate":"2021-07-28T19:00:00Z","endDate":"2021-07-28T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"IoT","startDate":"2021-09-03T14:00:00Z","endDate":"2021-09-03T17:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"IRS","startDate":"2021-09-08T19:00:00Z","endDate":"2021-09-08T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"MDLE","startDate":"2021-07-13T14:00:00Z","endDate":"2021-07-13T17:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"MDLE","startDate":"2021-07-19T14:00:00Z","endDate":"2021-07-19T17:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"MDLE","startDate":"2021-09-07T14:00:00Z","endDate":"2021-09-07T17:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"PIB","startDate":"2021-07-16T19:00:00Z","endDate":"2021-07-16T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"PIB","startDate":"2021-07-26T10:00:00Z","endDate":"2021-07-26T13:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"PIB","startDate":"2021-09-15T19:00:00Z","endDate":"2021-09-15T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"RCMov","startDate":"2021-07-12T19:00:00Z","endDate":"2021-07-12T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"RCMov","startDate":"2021-07-27T10:00:00Z","endDate":"2021-07-27T13:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"RCMov","startDate":"2021-09-11T10:00:00Z","endDate":"2021-09-11T13:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"RDC","startDate":"2021-09-08T19:00:00Z","endDate":"2021-09-08T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"RPC","startDate":"2021-07-01T19:00:00Z","endDate":"2021-07-01T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"RPC","startDate":"2021-07-29T19:00:00Z","endDate":"2021-07-29T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"RPC","startDate":"2021-09-17T14:00:00Z","endDate":"2021-09-17T17:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"SIAD","startDate":"2021-07-05T19:00:00Z","endDate":"2021-07-05T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"SIAD","startDate":"2021-07-21T14:00:00Z","endDate":"2021-07-21T17:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"SIAD","startDate":"2021-09-08T19:00:00Z","endDate":"2021-09-08T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"SRC","startDate":"2021-07-09T10:00:00Z","endDate":"2021-07-09T13:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"SRC","startDate":"2021-07-23T19:00:00Z","endDate":"2021-07-23T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"SRC","startDate":"2021-09-13T10:00:00Z","endDate":"2021-09-13T13:00:00Z","category":"EXAM_SPECIAL","location":""}]}""" - - assertEquals(json, serialized) - } - - @Test - fun `when Business object is equal to expected Dto then Success`() { - val academicCalendarDto = AcademicCalendarDto( - "20200706T160021Z", - "20210606T235001Z", - SchoolDto("Instituto Superior Engenharia Lisboa", "ISEL"), - "pt-PT", - listOf( - TermDto( - "2020-2021-1", - listOf( - EventDto( - "Interrupção de atividades letivas (Natal)", - "2020-12-21", - "2021-01-03" - ) - ), - listOf( - EvaluationDto( - "Período de exames (época normal)", - "2021-01-25", - "2021-02-13", - false - ) - ), - listOf(), - listOf( - EventDto( - "Divulgação de horários", - "2020-09-09", - "2020-09-09" - ), - EventDto( - "Abertura das atividades letivas 2020/2021", - "2020-09-28", - "2020-09-28" - ) - ) - ), - TermDto( - "2020-2021-2", - listOf( - EventDto( - "Interrupção de atividades letivas (Páscoa)", - "2021-03-29", - "2021-04-05" - ) - ), - listOf( - EvaluationDto( - "Período de exames (época normal)", - "2021-06-28", - "2021-07-17", - false - ), - EvaluationDto( - "Período de exames (época de recurso)", - "2021-07-19", - "2021-07-31", - false - ) - ), - listOf(), - listOf( - EventDto( - "Divulgação de horários", - "2021-02-22", - "2021-02-22" - ), - EventDto( - "Início das aulas", - "2021-03-15", - "2021-03-15" - ) + "2020-2021-2", + listOf( + Exam( + "AApl", + ZonedDateTime.of(2021, 7, 7, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 7, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "AApl", + ZonedDateTime.of(2021, 7, 24, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 24, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "AApl", + ZonedDateTime.of(2021, 9, 9, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 9, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "AC", + ZonedDateTime.of(2021, 7, 1, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 1, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "AC", + ZonedDateTime.of(2021, 7, 19, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 19, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "AC", + ZonedDateTime.of(2021, 9, 7, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 7, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "AED", + ZonedDateTime.of(2021, 6, 28, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 6, 28, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "AED", + ZonedDateTime.of(2021, 7, 20, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 20, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "AED", + ZonedDateTime.of(2021, 9, 16, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 16, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "ALGA", + ZonedDateTime.of(2021, 7, 8, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 8, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "ALGA", + ZonedDateTime.of(2021, 7, 23, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 23, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "ALGA", + ZonedDateTime.of(2021, 9, 8, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 8, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "AVE", + ZonedDateTime.of(2021, 7, 5, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 5, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "AVE", + ZonedDateTime.of(2021, 7, 22, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 22, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "AVE", + ZonedDateTime.of(2021, 9, 15, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 15, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "CDI", + ZonedDateTime.of(2021, 7, 16, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 16, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "CDI", + ZonedDateTime.of(2021, 7, 30, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 30, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "CDI", + ZonedDateTime.of(2021, 9, 17, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 17, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "CN", + ZonedDateTime.of(2021, 7, 15, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 15, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "CN", + ZonedDateTime.of(2021, 7, 29, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 29, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "CN", + ZonedDateTime.of(2021, 9, 10, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 10, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "COM", + ZonedDateTime.of(2021, 7, 12, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 12, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "COM", + ZonedDateTime.of(2021, 7, 26, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 26, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "COM", + ZonedDateTime.of(2021, 9, 13, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 13, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "CQF", + ZonedDateTime.of(2021, 9, 8, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 8, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "CSM", + ZonedDateTime.of(2021, 7, 12, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 12, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "CSM", + ZonedDateTime.of(2021, 7, 31, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 31, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "CSM", + ZonedDateTime.of(2021, 9, 8, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 8, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "DAW", + ZonedDateTime.of(2021, 6, 28, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 6, 28, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "DAW", + ZonedDateTime.of(2021, 7, 21, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 21, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "DAW", + ZonedDateTime.of(2021, 9, 15, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 15, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "EGP", + ZonedDateTime.of(2021, 7, 1, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 1, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "EGP", + ZonedDateTime.of(2021, 7, 20, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 20, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "EGP", + ZonedDateTime.of(2021, 9, 9, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 9, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "Eltr", + ZonedDateTime.of(2021, 7, 13, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 13, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "Eltr", + ZonedDateTime.of(2021, 7, 27, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 27, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "Eltr", + ZonedDateTime.of(2021, 9, 15, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 15, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "Emp", + ZonedDateTime.of(2021, 9, 16, 18, 30, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 16, 21, 30, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "GAP", + ZonedDateTime.of(2021, 6, 30, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 6, 30, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "GAP", + ZonedDateTime.of(2021, 7, 19, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 19, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "GAP", + ZonedDateTime.of(2021, 9, 3, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 3, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "GQS", + ZonedDateTime.of(2021, 9, 7, 18, 30, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 7, 21, 30, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "IASA", + ZonedDateTime.of(2021, 7, 2, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 2, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "IASA", + ZonedDateTime.of(2021, 7, 20, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 20, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "IASA", + ZonedDateTime.of(2021, 9, 2, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 2, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "IEB", + ZonedDateTime.of(2021, 9, 6, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 6, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "LC", + ZonedDateTime.of(2021, 9, 14, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 14, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "LSD", + ZonedDateTime.of(2021, 7, 6, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 6, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "LSD", + ZonedDateTime.of(2021, 7, 21, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 21, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "LSD", + ZonedDateTime.of(2021, 9, 6, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 6, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "PC", + ZonedDateTime.of(2021, 7, 13, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 13, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "PC", + ZonedDateTime.of(2021, 7, 27, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 27, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "PC", + ZonedDateTime.of(2021, 9, 17, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 17, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "PDM", + ZonedDateTime.of(2021, 9, 14, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 14, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "PE", + ZonedDateTime.of(2021, 7, 5, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 5, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "PE", + ZonedDateTime.of(2021, 7, 22, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 22, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "PE", + ZonedDateTime.of(2021, 9, 9, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 9, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "PG", + ZonedDateTime.of(2021, 6, 29, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 6, 29, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "PG", + ZonedDateTime.of(2021, 7, 23, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 23, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "PG", + ZonedDateTime.of(2021, 9, 8, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 8, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "PI", + ZonedDateTime.of(2021, 7, 2, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 2, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "PI", + ZonedDateTime.of(2021, 7, 21, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 21, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "PI", + ZonedDateTime.of(2021, 9, 10, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 10, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "POO", + ZonedDateTime.of(2021, 9, 14, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 14, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "PSC", + ZonedDateTime.of(2021, 7, 6, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 6, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "PSC", + ZonedDateTime.of(2021, 7, 21, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 21, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "PSC", + ZonedDateTime.of(2021, 9, 10, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 10, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "RCp", + ZonedDateTime.of(2021, 7, 14, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 14, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "RCp", + ZonedDateTime.of(2021, 7, 28, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 28, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "RCp", + ZonedDateTime.of(2021, 9, 13, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 13, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "RI", + ZonedDateTime.of(2021, 9, 7, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 7, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "SE1", + ZonedDateTime.of(2021, 9, 6, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 6, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "SE2", + ZonedDateTime.of(2021, 7, 9, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 9, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "SE2", + ZonedDateTime.of(2021, 7, 26, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 26, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "SE2", + ZonedDateTime.of(2021, 9, 6, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 6, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "SG", + ZonedDateTime.of(2021, 7, 1, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 1, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "SG", + ZonedDateTime.of(2021, 7, 20, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 20, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "SG", + ZonedDateTime.of(2021, 9, 8, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 8, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "SegInf", + ZonedDateTime.of(2021, 9, 17, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 17, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "SI1", + ZonedDateTime.of(2021, 6, 30, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 6, 30, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "SI1", + ZonedDateTime.of(2021, 7, 19, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 19, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "SI1", + ZonedDateTime.of(2021, 9, 3, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 3, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "SI2", + ZonedDateTime.of(2021, 7, 8, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 8, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "SI2", + ZonedDateTime.of(2021, 7, 23, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 23, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "SI2", + ZonedDateTime.of(2021, 9, 13, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 13, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "SM", + ZonedDateTime.of(2021, 9, 15, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 15, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "SO", + ZonedDateTime.of(2021, 7, 9, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 9, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "SO", + ZonedDateTime.of(2021, 7, 26, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 26, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "SO", + ZonedDateTime.of(2021, 9, 7, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 7, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "TAR", + ZonedDateTime.of(2021, 7, 16, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 16, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "TAR", + ZonedDateTime.of(2021, 7, 30, 19, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 30, 22, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "TAR", + ZonedDateTime.of(2021, 9, 7, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 7, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "TJ", + ZonedDateTime.of(2021, 7, 12, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 12, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "TJ", + ZonedDateTime.of(2021, 7, 28, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 28, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "TJ", + ZonedDateTime.of(2021, 9, 17, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 17, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" + ), + Exam( + "TMD", + ZonedDateTime.of(2021, 7, 10, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 10, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_NORMAL, + "" + ), + Exam( + "TMD", + ZonedDateTime.of(2021, 7, 29, 10, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 7, 29, 13, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_ALTERN, + "" + ), + Exam( + "TMD", + ZonedDateTime.of(2021, 9, 13, 14, 0, 0, 0, ZoneId.systemDefault()), + ZonedDateTime.of(2021, 9, 13, 17, 0, 0, 0, ZoneId.systemDefault()), + ExamCategory.EXAM_SPECIAL, + "" ) ) ) - ) + val serialized = mapper.writeValueAsString(EvaluationsDto.from(evaluations)) - val academicCalendarBO = AcademicCalendar( - "20200706T160021Z", - "20210606T235001Z", - School("Instituto Superior Engenharia Lisboa", "ISEL"), - Language.PT, - listOf( - Term( - "2020-2021-1", - listOf( - Event( - "Interrupção de atividades letivas (Natal)", - LocalDate.of(2020, Month.DECEMBER, 21), - LocalDate.of(2021, Month.JANUARY, 3) - ) - ), - listOf( - Evaluation( - "Período de exames (época normal)", - LocalDate.of(2021, Month.JANUARY, 25), - LocalDate.of(2021, Month.FEBRUARY, 13), - false - ) - ), - listOf(), - listOf( - Event( - "Divulgação de horários", - LocalDate.of(2020, Month.SEPTEMBER, 9), - LocalDate.of(2020, Month.SEPTEMBER, 9) - ), - Event( - "Abertura das atividades letivas 2020/2021", - LocalDate.of(2020, Month.SEPTEMBER, 28), - LocalDate.of(2020, Month.SEPTEMBER, 28) - ) - ) - ), - Term( - "2020-2021-2", - listOf( - Event( - "Interrupção de atividades letivas (Páscoa)", - LocalDate.of(2021, Month.MARCH, 29), - LocalDate.of(2021, Month.APRIL, 5) - ) - ), - listOf( - Evaluation( - "Período de exames (época normal)", - LocalDate.of(2021, Month.JUNE, 28), - LocalDate.of(2021, Month.JULY, 17), - false - ), - Evaluation( - "Período de exames (época de recurso)", - LocalDate.of(2021, Month.JULY, 19), - LocalDate.of(2021, Month.JULY, 31), - false - ) - ), - listOf(), - listOf( - Event( - "Divulgação de horários", - LocalDate.of(2021, Month.FEBRUARY, 22), - LocalDate.of(2021, Month.FEBRUARY, 22) - ), - Event( - "Início das aulas", - LocalDate.of(2021, Month.MARCH, 15), - LocalDate.of(2021, Month.MARCH, 15) - ) - ) - ) - ) - ) - assertEquals(AcademicCalendarDto.from(academicCalendarBO), academicCalendarDto) + val expectedJson = + """{"creationDateTime":"2021-07-08T00:12:56Z","retrievalDateTime":"2021-07-08T00:12:56Z","school":{"name":"Instituto Superior de Engenharia de Lisboa","acr":"ISEL"},"programme":{"name":"Licenciatura em Engenharia Informática e de Computadores","acr":"LEIC"},"calendarTerm":"2020-2021-2","exams":[{"course":"AApl","startDate":"2021-07-07T10:00:00Z","endDate":"2021-07-07T13:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"AApl","startDate":"2021-07-24T10:00:00Z","endDate":"2021-07-24T13:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"AApl","startDate":"2021-09-09T14:00:00Z","endDate":"2021-09-09T17:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"AC","startDate":"2021-07-01T14:00:00Z","endDate":"2021-07-01T17:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"AC","startDate":"2021-07-19T19:00:00Z","endDate":"2021-07-19T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"AC","startDate":"2021-09-07T10:00:00Z","endDate":"2021-09-07T13:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"AED","startDate":"2021-06-28T19:00:00Z","endDate":"2021-06-28T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"AED","startDate":"2021-07-20T14:00:00Z","endDate":"2021-07-20T17:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"AED","startDate":"2021-09-16T19:00:00Z","endDate":"2021-09-16T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"ALGA","startDate":"2021-07-08T19:00:00Z","endDate":"2021-07-08T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"ALGA","startDate":"2021-07-23T10:00:00Z","endDate":"2021-07-23T13:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"ALGA","startDate":"2021-09-08T19:00:00Z","endDate":"2021-09-08T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"AVE","startDate":"2021-07-05T19:00:00Z","endDate":"2021-07-05T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"AVE","startDate":"2021-07-22T10:00:00Z","endDate":"2021-07-22T13:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"AVE","startDate":"2021-09-15T14:00:00Z","endDate":"2021-09-15T17:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"CDI","startDate":"2021-07-16T19:00:00Z","endDate":"2021-07-16T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"CDI","startDate":"2021-07-30T19:00:00Z","endDate":"2021-07-30T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"CDI","startDate":"2021-09-17T19:00:00Z","endDate":"2021-09-17T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"CN","startDate":"2021-07-15T10:00:00Z","endDate":"2021-07-15T13:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"CN","startDate":"2021-07-29T19:00:00Z","endDate":"2021-07-29T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"CN","startDate":"2021-09-10T19:00:00Z","endDate":"2021-09-10T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"COM","startDate":"2021-07-12T19:00:00Z","endDate":"2021-07-12T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"COM","startDate":"2021-07-26T14:00:00Z","endDate":"2021-07-26T17:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"COM","startDate":"2021-09-13T19:00:00Z","endDate":"2021-09-13T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"CQF","startDate":"2021-09-08T10:00:00Z","endDate":"2021-09-08T13:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"CSM","startDate":"2021-07-12T19:00:00Z","endDate":"2021-07-12T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"CSM","startDate":"2021-07-31T10:00:00Z","endDate":"2021-07-31T13:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"CSM","startDate":"2021-09-08T10:00:00Z","endDate":"2021-09-08T13:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"DAW","startDate":"2021-06-28T19:00:00Z","endDate":"2021-06-28T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"DAW","startDate":"2021-07-21T10:00:00Z","endDate":"2021-07-21T13:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"DAW","startDate":"2021-09-15T19:00:00Z","endDate":"2021-09-15T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"EGP","startDate":"2021-07-01T19:00:00Z","endDate":"2021-07-01T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"EGP","startDate":"2021-07-20T14:00:00Z","endDate":"2021-07-20T17:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"EGP","startDate":"2021-09-09T19:00:00Z","endDate":"2021-09-09T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"Eltr","startDate":"2021-07-13T19:00:00Z","endDate":"2021-07-13T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"Eltr","startDate":"2021-07-27T10:00:00Z","endDate":"2021-07-27T13:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"Eltr","startDate":"2021-09-15T19:00:00Z","endDate":"2021-09-15T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"Emp","startDate":"2021-09-16T18:30:00Z","endDate":"2021-09-16T21:30:00Z","category":"EXAM_SPECIAL","location":""},{"course":"GAP","startDate":"2021-06-30T10:00:00Z","endDate":"2021-06-30T13:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"GAP","startDate":"2021-07-19T10:00:00Z","endDate":"2021-07-19T13:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"GAP","startDate":"2021-09-03T14:00:00Z","endDate":"2021-09-03T17:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"GQS","startDate":"2021-09-07T18:30:00Z","endDate":"2021-09-07T21:30:00Z","category":"EXAM_SPECIAL","location":""},{"course":"IASA","startDate":"2021-07-02T10:00:00Z","endDate":"2021-07-02T13:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"IASA","startDate":"2021-07-20T19:00:00Z","endDate":"2021-07-20T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"IASA","startDate":"2021-09-02T19:00:00Z","endDate":"2021-09-02T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"IEB","startDate":"2021-09-06T10:00:00Z","endDate":"2021-09-06T13:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"LC","startDate":"2021-09-14T10:00:00Z","endDate":"2021-09-14T13:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"LSD","startDate":"2021-07-06T10:00:00Z","endDate":"2021-07-06T13:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"LSD","startDate":"2021-07-21T19:00:00Z","endDate":"2021-07-21T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"LSD","startDate":"2021-09-06T19:00:00Z","endDate":"2021-09-06T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"PC","startDate":"2021-07-13T19:00:00Z","endDate":"2021-07-13T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"PC","startDate":"2021-07-27T19:00:00Z","endDate":"2021-07-27T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"PC","startDate":"2021-09-17T19:00:00Z","endDate":"2021-09-17T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"PDM","startDate":"2021-09-14T19:00:00Z","endDate":"2021-09-14T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"PE","startDate":"2021-07-05T10:00:00Z","endDate":"2021-07-05T13:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"PE","startDate":"2021-07-22T19:00:00Z","endDate":"2021-07-22T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"PE","startDate":"2021-09-09T19:00:00Z","endDate":"2021-09-09T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"PG","startDate":"2021-06-29T19:00:00Z","endDate":"2021-06-29T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"PG","startDate":"2021-07-23T14:00:00Z","endDate":"2021-07-23T17:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"PG","startDate":"2021-09-08T14:00:00Z","endDate":"2021-09-08T17:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"PI","startDate":"2021-07-02T19:00:00Z","endDate":"2021-07-02T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"PI","startDate":"2021-07-21T19:00:00Z","endDate":"2021-07-21T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"PI","startDate":"2021-09-10T19:00:00Z","endDate":"2021-09-10T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"POO","startDate":"2021-09-14T19:00:00Z","endDate":"2021-09-14T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"PSC","startDate":"2021-07-06T19:00:00Z","endDate":"2021-07-06T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"PSC","startDate":"2021-07-21T14:00:00Z","endDate":"2021-07-21T17:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"PSC","startDate":"2021-09-10T19:00:00Z","endDate":"2021-09-10T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"RCp","startDate":"2021-07-14T10:00:00Z","endDate":"2021-07-14T13:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"RCp","startDate":"2021-07-28T19:00:00Z","endDate":"2021-07-28T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"RCp","startDate":"2021-09-13T19:00:00Z","endDate":"2021-09-13T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"RI","startDate":"2021-09-07T14:00:00Z","endDate":"2021-09-07T17:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"SE1","startDate":"2021-09-06T19:00:00Z","endDate":"2021-09-06T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"SE2","startDate":"2021-07-09T19:00:00Z","endDate":"2021-07-09T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"SE2","startDate":"2021-07-26T10:00:00Z","endDate":"2021-07-26T13:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"SE2","startDate":"2021-09-06T19:00:00Z","endDate":"2021-09-06T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"SG","startDate":"2021-07-01T10:00:00Z","endDate":"2021-07-01T13:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"SG","startDate":"2021-07-20T19:00:00Z","endDate":"2021-07-20T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"SG","startDate":"2021-09-08T14:00:00Z","endDate":"2021-09-08T17:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"SegInf","startDate":"2021-09-17T14:00:00Z","endDate":"2021-09-17T17:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"SI1","startDate":"2021-06-30T19:00:00Z","endDate":"2021-06-30T22:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"SI1","startDate":"2021-07-19T14:00:00Z","endDate":"2021-07-19T17:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"SI1","startDate":"2021-09-03T19:00:00Z","endDate":"2021-09-03T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"SI2","startDate":"2021-07-08T14:00:00Z","endDate":"2021-07-08T17:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"SI2","startDate":"2021-07-23T19:00:00Z","endDate":"2021-07-23T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"SI2","startDate":"2021-09-13T10:00:00Z","endDate":"2021-09-13T13:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"SM","startDate":"2021-09-15T10:00:00Z","endDate":"2021-09-15T13:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"SO","startDate":"2021-07-09T10:00:00Z","endDate":"2021-07-09T13:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"SO","startDate":"2021-07-26T19:00:00Z","endDate":"2021-07-26T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"SO","startDate":"2021-09-07T19:00:00Z","endDate":"2021-09-07T22:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"TAR","startDate":"2021-07-16T10:00:00Z","endDate":"2021-07-16T13:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"TAR","startDate":"2021-07-30T19:00:00Z","endDate":"2021-07-30T22:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"TAR","startDate":"2021-09-07T10:00:00Z","endDate":"2021-09-07T13:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"TJ","startDate":"2021-07-12T10:00:00Z","endDate":"2021-07-12T13:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"TJ","startDate":"2021-07-28T10:00:00Z","endDate":"2021-07-28T13:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"TJ","startDate":"2021-09-17T14:00:00Z","endDate":"2021-09-17T17:00:00Z","category":"EXAM_SPECIAL","location":""},{"course":"TMD","startDate":"2021-07-10T10:00:00Z","endDate":"2021-07-10T13:00:00Z","category":"EXAM_NORMAL","location":""},{"course":"TMD","startDate":"2021-07-29T10:00:00Z","endDate":"2021-07-29T13:00:00Z","category":"EXAM_ALTERN","location":""},{"course":"TMD","startDate":"2021-09-13T14:00:00Z","endDate":"2021-09-13T17:00:00Z","category":"EXAM_SPECIAL","location":""}]}""" + + assertEquals(expectedJson, serialized) } } From 6f9e7194f2927b85f55d7a59d3cf9ff0df359a7e Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Fri, 9 Jul 2021 12:13:50 +0100 Subject: [PATCH 66/67] Improvement on calls to trimCourse. --- .../integration/domain/evaluations/BusinessObjects.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt index 00485a3f..e0bb83a7 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt @@ -84,6 +84,7 @@ data class Evaluations( for (table in tableList) { for (line in table.data) { val cleanedLine = line.dropWhile { it.text.isBlank() } + val courseAcronym = trimCourse(cleanedLine[TableColumn.COURSE.ordinal].text) if (cleanedLine[TableColumn.SUMMER_EXAM_PROGRAMME.ordinal].text.contains(programmeAcronym)) { val intervalDateTimeNormal = DateUtils.getEvaluationDateTimeFrom( @@ -94,7 +95,7 @@ data class Evaluations( ) examList.add( Exam( - trimCourse(cleanedLine[TableColumn.COURSE.ordinal].text), + courseAcronym, intervalDateTimeNormal.from, intervalDateTimeNormal.to, ExamCategory.EXAM_NORMAL, @@ -110,7 +111,7 @@ data class Evaluations( ) examList.add( Exam( - trimCourse(cleanedLine[TableColumn.COURSE.ordinal].text), + courseAcronym, intervalDateTimeAltern.from, intervalDateTimeAltern.to, ExamCategory.EXAM_ALTERN, @@ -126,7 +127,7 @@ data class Evaluations( ) examList.add( Exam( - trimCourse(cleanedLine[TableColumn.COURSE.ordinal].text), + courseAcronym, intervalDateTimeSpecial.from, intervalDateTimeSpecial.to, ExamCategory.EXAM_SPECIAL, From 10d054990807de56e2eefe7a0ec54706d2818f25 Mon Sep 17 00:00:00 2001 From: Ricardo Canto Date: Fri, 9 Jul 2021 12:49:31 +0100 Subject: [PATCH 67/67] Improvement on calls to trimCourse. --- .../integration/domain/evaluations/BusinessObjects.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt index e0bb83a7..14a0c832 100644 --- a/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt +++ b/src/main/kotlin/org/ionproject/integration/domain/evaluations/BusinessObjects.kt @@ -147,7 +147,7 @@ data class Evaluations( ) examList.add( Exam( - trimCourse(cleanedLine[TableColumn.COURSE.ordinal].text), + courseAcronym, intervalDateTimeSpecial.from, intervalDateTimeSpecial.to, ExamCategory.EXAM_SPECIAL,