From 6c115fb915cfcc29c1a25e3f94476c473a4e1ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Fri, 18 Nov 2022 16:32:04 +0100 Subject: [PATCH] Fallback to subject name from timetable when attendance item doesn't have it (#2052) * Fallback to subject name from timetable when attendance item doesn't have it * Fix tests * Add some unit tests --- .../java/io/github/wulkanowy/data/Resource.kt | 2 +- .../wulkanowy/data/db/dao/TimetableDao.kt | 3 + .../data/mappers/AttendanceMapper.kt | 9 +- .../data/repositories/AttendanceRepository.kt | 11 +- .../data/mappers/AttendanceMapperKtTest.kt | 143 ++++++++++++++++++ .../repositories/AttendanceRepositoryTest.kt | 27 ++-- 6 files changed, 180 insertions(+), 15 deletions(-) create mode 100644 app/src/test/java/io/github/wulkanowy/data/mappers/AttendanceMapperKtTest.kt diff --git a/app/src/main/java/io/github/wulkanowy/data/Resource.kt b/app/src/main/java/io/github/wulkanowy/data/Resource.kt index 44f8a1b4..6b611e47 100644 --- a/app/src/main/java/io/github/wulkanowy/data/Resource.kt +++ b/app/src/main/java/io/github/wulkanowy/data/Resource.kt @@ -49,8 +49,8 @@ fun Resource.mapData(block: (T) -> U) = when (this) { fun Flow>.logResourceStatus(name: String, showData: Boolean = false) = onEach { val description = when (it) { - is Resource.Loading -> "started" is Resource.Intermediate -> "intermediate data received" + if (showData) " (data: `${it.data}`)" else "" + is Resource.Loading -> "started" is Resource.Success -> "success" + if (showData) " (data: `${it.data}`)" else "" is Resource.Error -> "exception occurred: ${it.error}" } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt index 5e6eec66..b4b7379f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt @@ -13,4 +13,7 @@ interface TimetableDao : BaseDao { @Query("SELECT * FROM Timetable WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end") fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow> + + @Query("SELECT * FROM Timetable WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end") + fun load(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): List } diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/AttendanceMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/AttendanceMapper.kt index 46e67fda..c0ed0c8c 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/AttendanceMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/AttendanceMapper.kt @@ -3,17 +3,22 @@ package io.github.wulkanowy.data.mappers import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.AttendanceSummary import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.sdk.pojo.Attendance as SdkAttendance import io.github.wulkanowy.sdk.pojo.AttendanceSummary as SdkAttendanceSummary -fun List.mapToEntities(semester: Semester) = map { +fun List.mapToEntities(semester: Semester, lessons: List) = map { Attendance( studentId = semester.studentId, diaryId = semester.diaryId, date = it.date, timeId = it.timeId, number = it.number, - subject = it.subject, + subject = it.subject.ifBlank { + lessons.find { lesson -> + lesson.date == it.date && lesson.number == it.number + }?.subject.orEmpty() + }, name = it.name, presence = it.presence, absence = it.absence, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt index 9aa6562a..fd5d8bd1 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.data.repositories import io.github.wulkanowy.data.db.dao.AttendanceDao +import io.github.wulkanowy.data.db.dao.TimetableDao import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student @@ -9,8 +10,10 @@ import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Absent import io.github.wulkanowy.utils.* +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.withContext import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime @@ -20,6 +23,7 @@ import javax.inject.Singleton @Singleton class AttendanceRepository @Inject constructor( private val attendanceDb: AttendanceDao, + private val timetableDb: TimetableDao, private val sdk: Sdk, private val refreshHelper: AutoRefreshHelper, ) { @@ -48,10 +52,15 @@ class AttendanceRepository @Inject constructor( attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) }, fetch = { + val lessons = withContext(Dispatchers.IO) { + timetableDb.load( + semester.diaryId, semester.studentId, start.monday, end.sunday + ) + } sdk.init(student) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getAttendance(start.monday, end.sunday, semester.semesterId) - .mapToEntities(semester) + .mapToEntities(semester, lessons) }, saveFetchResult = { old, new -> attendanceDb.deleteAll(old uniqueSubtract new) diff --git a/app/src/test/java/io/github/wulkanowy/data/mappers/AttendanceMapperKtTest.kt b/app/src/test/java/io/github/wulkanowy/data/mappers/AttendanceMapperKtTest.kt new file mode 100644 index 00000000..a35e5d30 --- /dev/null +++ b/app/src/test/java/io/github/wulkanowy/data/mappers/AttendanceMapperKtTest.kt @@ -0,0 +1,143 @@ +package io.github.wulkanowy.data.mappers + +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.sdk.pojo.Attendance +import io.github.wulkanowy.sdk.scrapper.attendance.SentExcuse +import org.junit.Test +import java.time.Instant +import java.time.LocalDate +import kotlin.test.assertEquals + +class AttendanceMapperTest { + + @Test + fun `map attendance when fallback is not necessary`() { + val attendance = listOf( + getSdkAttendance(1, LocalDate.of(2022, 11, 17), "Oryginalna 1"), + getSdkAttendance(2, LocalDate.of(2022, 11, 17), "Oryginalna 2"), + ) + val lessons = listOf( + getEntityTimetable(1, LocalDate.of(2022, 11, 17), "Pierwsza"), + getEntityTimetable(2, LocalDate.of(2022, 11, 17), "Druga"), + ) + + val result = attendance.mapToEntities(getEntitySemester(), lessons) + assertEquals("Oryginalna 1", result[0].subject) + assertEquals("Oryginalna 2", result[1].subject) + } + + @Test + fun `map attendance when fallback is not always necessary`() { + val attendance = listOf( + getSdkAttendance(1, LocalDate.of(2022, 11, 17), "Oryginalna 1"), + getSdkAttendance(2, LocalDate.of(2022, 11, 17), ""), + ) + val lessons = listOf( + getEntityTimetable(1, LocalDate.of(2022, 11, 17), "Pierwsza"), + getEntityTimetable(2, LocalDate.of(2022, 11, 17), "Druga"), + ) + + val result = attendance.mapToEntities(getEntitySemester(), lessons) + assertEquals("Oryginalna 1", result[0].subject) + assertEquals("Druga", result[1].subject) + } + + @Test + fun `map attendance when fallback is sometimes empty`() { + val attendance = listOf( + getSdkAttendance(1, LocalDate.of(2022, 11, 17), "Oryginalna 1"), + getSdkAttendance(2, LocalDate.of(2022, 11, 17), ""), + ) + val lessons = listOf( + getEntityTimetable(1, LocalDate.of(2022, 11, 17), "Pierwsza"), + ) + + val result = attendance.mapToEntities(getEntitySemester(), lessons) + assertEquals("Oryginalna 1", result[0].subject) + assertEquals("", result[1].subject) + } + + @Test + fun `map attendance when fallback is empty`() { + val attendance = listOf( + getSdkAttendance(1, LocalDate.of(2022, 11, 17), ""), + getSdkAttendance(2, LocalDate.of(2022, 11, 17), ""), + ) + val lessons = listOf( + getEntityTimetable(1, LocalDate.of(2022, 11, 18), "Pierwsza"), + getEntityTimetable(2, LocalDate.of(2022, 10, 17), "Druga"), + ) + + val result = attendance.mapToEntities(getEntitySemester(), lessons) + assertEquals("", result[0].subject) + assertEquals("", result[1].subject) + } + + @Test + fun `map attendance with all subject fallback`() { + val attendance = listOf( + getSdkAttendance(1, LocalDate.of(2022, 11, 17)), + getSdkAttendance(2, LocalDate.of(2022, 11, 17)), + ) + val lessons = listOf( + getEntityTimetable(1, LocalDate.of(2022, 11, 17), "Pierwsza"), + getEntityTimetable(2, LocalDate.of(2022, 11, 17), "Druga"), + ) + + val result = attendance.mapToEntities(getEntitySemester(), lessons) + assertEquals("Pierwsza", result[0].subject) + assertEquals("Druga", result[1].subject) + } + + private fun getSdkAttendance(number: Int, date: LocalDate, subject: String = "") = Attendance( + number = number, + name = "ABSENCE", + subject = subject, + date = date, + timeId = 1, + categoryId = 1, + deleted = false, + excuseStatus = SentExcuse.Status.WAITING, + excusable = false, + absence = false, + excused = false, + exemption = false, + lateness = false, + presence = false, + ) + + private fun getEntityTimetable(number: Int, date: LocalDate, subject: String = "") = Timetable( + number = number, + start = Instant.now(), + end = Instant.now(), + date = date, + subject = subject, + subjectOld = "", + group = "", + room = "", + roomOld = "", + teacher = "", + teacherOld = "", + info = "", + changes = false, + canceled = false, + studentId = 0, + diaryId = 0, + isStudentPlan = false, + ) + + private fun getEntitySemester() = Semester( + studentId = 0, + diaryId = 0, + kindergartenDiaryId = 0, + diaryName = "", + schoolYear = 0, + semesterId = 0, + semesterName = 0, + start = LocalDate.now(), + end = LocalDate.now(), + classId = 0, + unitId = 0 + ) +} diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt index 7d22f726..896491ef 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy.data.repositories import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.AttendanceDao +import io.github.wulkanowy.data.db.dao.TimetableDao import io.github.wulkanowy.data.errorOrNull import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.toFirstResult @@ -29,6 +30,9 @@ class AttendanceRepositoryTest { @MockK private lateinit var attendanceDb: AttendanceDao + @MockK + private lateinit var timetableDb: TimetableDao + @MockK(relaxUnitFun = true) private lateinit var refreshHelper: AutoRefreshHelper @@ -51,8 +55,9 @@ class AttendanceRepositoryTest { fun setUp() { MockKAnnotations.init(this) every { refreshHelper.shouldBeRefreshed(any()) } returns false + coEvery { timetableDb.load(any(), any(), any(), any()) } returns emptyList() - attendanceRepository = AttendanceRepository(attendanceDb, sdk, refreshHelper) + attendanceRepository = AttendanceRepository(attendanceDb, timetableDb, sdk, refreshHelper) } @Test @@ -60,8 +65,8 @@ class AttendanceRepositoryTest { // prepare coEvery { sdk.getAttendance(startDate, endDate, 1) } returns remoteList coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( - flowOf(remoteList.mapToEntities(semester)), - flowOf(remoteList.mapToEntities(semester)) + flowOf(remoteList.mapToEntities(semester, emptyList())), + flowOf(remoteList.mapToEntities(semester, emptyList())) ) coEvery { attendanceDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { attendanceDb.deleteAll(any()) } just Runs @@ -83,9 +88,9 @@ class AttendanceRepositoryTest { // prepare coEvery { sdk.getAttendance(startDate, endDate, 1) } returns remoteList coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( - flowOf(remoteList.dropLast(1).mapToEntities(semester)), - flowOf(remoteList.dropLast(1).mapToEntities(semester)), // after fetch end before save result - flowOf(remoteList.mapToEntities(semester)) + flowOf(remoteList.dropLast(1).mapToEntities(semester, emptyList())), + flowOf(remoteList.dropLast(1).mapToEntities(semester, emptyList())), // after fetch end before save result + flowOf(remoteList.mapToEntities(semester, emptyList())) ) coEvery { attendanceDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { attendanceDb.deleteAll(any()) } just Runs @@ -100,7 +105,7 @@ class AttendanceRepositoryTest { coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { attendanceDb.insertAll(match { - it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] + it.size == 1 && it[0] == remoteList.mapToEntities(semester, emptyList())[1] }) } coVerify { attendanceDb.deleteAll(match { it.isEmpty() }) } @@ -111,9 +116,9 @@ class AttendanceRepositoryTest { // prepare coEvery { sdk.getAttendance(startDate, endDate, 1) } returns remoteList.dropLast(1) coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( - flowOf(remoteList.mapToEntities(semester)), - flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result - flowOf(remoteList.dropLast(1).mapToEntities(semester)) + flowOf(remoteList.mapToEntities(semester, emptyList())), + flowOf(remoteList.mapToEntities(semester, emptyList())), // after fetch end before save result + flowOf(remoteList.dropLast(1).mapToEntities(semester, emptyList())) ) coEvery { attendanceDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { attendanceDb.deleteAll(any()) } just Runs @@ -129,7 +134,7 @@ class AttendanceRepositoryTest { coVerify { attendanceDb.insertAll(match { it.isEmpty() }) } coVerify { attendanceDb.deleteAll(match { - it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] + it.size == 1 && it[0] == remoteList.mapToEntities(semester, emptyList())[1] }) } }