Optimize grade average provider (#792)

This commit is contained in:
Mikołaj Pich 2020-05-10 10:51:01 +02:00 committed by GitHub
parent 8eb0c0351b
commit 6d1fa0cf05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 278 additions and 324 deletions

View File

@ -122,7 +122,7 @@ configurations.all {
}
dependencies {
implementation "io.github.wulkanowy:sdk:0.17.4"
implementation "io.github.wulkanowy:sdk:445905b"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.core:core-ktx:1.2.0"

View File

@ -24,7 +24,7 @@ class GradeLocalTest {
fun createDb() {
testDb = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), AppDatabase::class.java)
.build()
gradeLocal = GradeLocal(testDb.gradeDao)
gradeLocal = GradeLocal(testDb.gradeDao, testDb.gradeSummaryDao)
}
@After
@ -43,7 +43,7 @@ class GradeLocalTest {
val semester = Semester(1, 2, "", 2019, 2, 1, now(), now(), 1, 1)
val grades = gradeLocal
.getGrades(semester)
.getGradesDetails(semester)
.blockingGet()
assertEquals(2, grades.size)

View File

@ -11,6 +11,7 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.TestInternetObservingStrategy
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Grade
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
@ -52,7 +53,7 @@ class GradeRepositoryTest {
fun initApi() {
MockKAnnotations.init(this)
testDb = Room.inMemoryDatabaseBuilder(getApplicationContext(), AppDatabase::class.java).build()
gradeLocal = GradeLocal(testDb.gradeDao)
gradeLocal = GradeLocal(testDb.gradeDao, testDb.gradeSummaryDao)
gradeRemote = GradeRemote(mockSdk)
every { studentMock.registrationDate } returns LocalDateTime.of(2019, 2, 27, 12, 0)
@ -75,10 +76,10 @@ class GradeRepositoryTest {
createGradeApi(5, 4.0, of(2019, 2, 26), "przed zalogowanie w aplikacji"),
createGradeApi(5, 4.0, of(2019, 2, 27), "Ocena z dnia logowania"),
createGradeApi(5, 4.0, of(2019, 2, 28), "Ocena jeszcze nowsza")
))
) to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet().sortedByDescending { it.date }
.getGrades(studentMock, semesterMock, true).blockingGet().first.sortedByDescending { it.date }
assertFalse { grades[0].isRead }
assertFalse { grades[1].isRead }
@ -99,10 +100,10 @@ class GradeRepositoryTest {
createGradeApi(4, 3.0, of(2019, 2, 26), "starszą niż ostatnia lokalnie"),
createGradeApi(3, 4.0, of(2019, 2, 27), "Ta jest z tego samego dnia co ostatnia lokalnie"),
createGradeApi(2, 5.0, of(2019, 2, 28), "Ta jest już w ogóle nowa")
))
) to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet().sortedByDescending { it.date }
.getGrades(studentMock, semesterMock, true).blockingGet().first.sortedByDescending { it.date }
assertFalse { grades[0].isRead }
assertFalse { grades[1].isRead }
@ -121,12 +122,12 @@ class GradeRepositoryTest {
every { mockSdk.getGrades(1) } returns Single.just(listOf(
createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
))
) to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet()
assertEquals(2, grades.size)
assertEquals(2, grades.first.size)
}
@Test
@ -140,12 +141,12 @@ class GradeRepositoryTest {
createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
))
) to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet()
assertEquals(3, grades.size)
assertEquals(3, grades.first.size)
}
@Test
@ -156,12 +157,12 @@ class GradeRepositoryTest {
createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
))
) to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet()
assertEquals(3, grades.size)
assertEquals(3, grades.first.size)
}
@Test
@ -171,11 +172,11 @@ class GradeRepositoryTest {
createGradeLocal(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
))
every { mockSdk.getGrades(1) } returns Single.just(listOf())
every { mockSdk.getGrades(1) } returns Single.just(emptyList<Grade>() to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet()
assertEquals(0, grades.size)
assertEquals(0, grades.first.size)
}
}

View File

@ -1,14 +1,19 @@
package io.github.wulkanowy.data.repositories.grade
import io.github.wulkanowy.data.db.dao.GradeDao
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.reactivex.Maybe
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GradeLocal @Inject constructor(private val gradeDb: GradeDao) {
class GradeLocal @Inject constructor(
private val gradeDb: GradeDao,
private val gradeSummaryDb: GradeSummaryDao
) {
fun saveGrades(grades: List<Grade>) {
gradeDb.insertAll(grades)
@ -22,7 +27,19 @@ class GradeLocal @Inject constructor(private val gradeDb: GradeDao) {
gradeDb.updateAll(grades)
}
fun getGrades(semester: Semester): Maybe<List<Grade>> {
fun getGradesDetails(semester: Semester): Maybe<List<Grade>> {
return gradeDb.loadAll(semester.semesterId, semester.studentId).filter { it.isNotEmpty() }
}
fun saveGradesSummary(gradesSummary: List<GradeSummary>) {
gradeSummaryDb.insertAll(gradesSummary)
}
fun deleteGradesSummary(gradesSummary: List<GradeSummary>) {
gradeSummaryDb.deleteAll(gradesSummary)
}
fun getGradesSummary(semester: Semester): Maybe<List<GradeSummary>> {
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).filter { it.isNotEmpty() }
}
}

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.data.repositories.grade
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.Sdk
@ -12,11 +13,11 @@ import javax.inject.Singleton
@Singleton
class GradeRemote @Inject constructor(private val sdk: Sdk) {
fun getGrades(student: Student, semester: Semester): Single<List<Grade>> {
fun getGrades(student: Student, semester: Semester): Single<Pair<List<Grade>, List<GradeSummary>>> {
return sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getGrades(semester.semesterId)
.map { grades ->
grades.map {
.map { (details, summary) ->
details.map {
Grade(
studentId = semester.studentId,
semesterId = semester.semesterId,
@ -33,6 +34,19 @@ class GradeRemote @Inject constructor(private val sdk: Sdk) {
date = it.date,
teacher = it.teacher
)
} to summary.map {
GradeSummary(
semesterId = semester.semesterId,
studentId = semester.studentId,
position = 0,
subject = it.name,
predictedGrade = it.predicted,
finalGrade = it.final,
pointsSum = it.pointsSum,
proposedPoints = it.proposedPoints,
finalPoints = it.finalPoints,
average = it.average
)
}
}
}

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.data.repositories.grade
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.uniqueSubtract
@ -19,34 +20,47 @@ class GradeRepository @Inject constructor(
private val remote: GradeRemote
) {
fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean = false, notify: Boolean = false): Single<List<Grade>> {
return local.getGrades(semester).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean = false, notify: Boolean = false): Single<Pair<List<Grade>, List<GradeSummary>>> {
return local.getGradesDetails(semester).flatMap { details ->
local.getGradesSummary(semester).map { summary -> details to summary }
}.filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getGrades(student, semester)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getGrades(semester).toSingle(emptyList())
}.flatMap { (newDetails, newSummary) ->
local.getGradesDetails(semester).toSingle(emptyList())
.doOnSuccess { old ->
val notifyBreakDate = old.maxBy { it.date }?.date ?: student.registrationDate.toLocalDate()
local.deleteGrades(old.uniqueSubtract(new))
local.saveGrades(new.uniqueSubtract(old)
local.deleteGrades(old.uniqueSubtract(newDetails))
local.saveGrades(newDetails.uniqueSubtract(old)
.onEach {
if (it.date >= notifyBreakDate) it.apply {
isRead = false
if (notify) isNotified = false
}
})
}.flatMap {
local.getGradesSummary(semester).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteGradesSummary(old.uniqueSubtract(newSummary))
local.saveGradesSummary(newSummary.uniqueSubtract(old))
}
}.flatMap { local.getGrades(semester).toSingle(emptyList()) })
}
}.flatMap {
local.getGradesDetails(semester).toSingle(emptyList()).flatMap { details ->
local.getGradesSummary(semester).toSingle(emptyList()).map { summary ->
details to summary
}
}
})
}
fun getUnreadGrades(semester: Semester): Single<List<Grade>> {
return local.getGrades(semester).map { it.filter { grade -> !grade.isRead } }.toSingle(emptyList())
return local.getGradesDetails(semester).map { it.filter { grade -> !grade.isRead } }.toSingle(emptyList())
}
fun getNotNotifiedGrades(semester: Semester): Single<List<Grade>> {
return local.getGrades(semester).map { it.filter { grade -> !grade.isNotified } }.toSingle(emptyList())
return local.getGradesDetails(semester).map { it.filter { grade -> !grade.isNotified } }.toSingle(emptyList())
}
fun updateGrade(grade: Grade): Completable {

View File

@ -1,24 +0,0 @@
package io.github.wulkanowy.data.repositories.gradessummary
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.reactivex.Maybe
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GradeSummaryLocal @Inject constructor(private val gradeSummaryDb: GradeSummaryDao) {
fun saveGradesSummary(gradesSummary: List<GradeSummary>) {
gradeSummaryDb.insertAll(gradesSummary)
}
fun deleteGradesSummary(gradesSummary: List<GradeSummary>) {
gradeSummaryDb.deleteAll(gradesSummary)
}
fun getGradesSummary(semester: Semester): Maybe<List<GradeSummary>> {
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).filter { it.isNotEmpty() }
}
}

View File

@ -1,35 +0,0 @@
package io.github.wulkanowy.data.repositories.gradessummary
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init
import io.reactivex.Single
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GradeSummaryRemote @Inject constructor(private val sdk: Sdk) {
fun getGradeSummary(student: Student, semester: Semester): Single<List<GradeSummary>> {
return sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getGradesSummary(semester.semesterId)
.map { gradesSummary ->
gradesSummary.map {
GradeSummary(
semesterId = semester.semesterId,
studentId = semester.studentId,
position = 0,
subject = it.name,
predictedGrade = it.predicted,
finalGrade = it.final,
pointsSum = it.pointsSum,
proposedPoints = it.proposedPoints,
finalPoints = it.finalPoints,
average = it.average
)
}
}
}
}

View File

@ -1,35 +0,0 @@
package io.github.wulkanowy.data.repositories.gradessummary
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GradeSummaryRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: GradeSummaryLocal,
private val remote: GradeSummaryRemote
) {
fun getGradesSummary(student: Student, semester: Semester, forceRefresh: Boolean = false): Single<List<GradeSummary>> {
return local.getGradesSummary(semester).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getGradeSummary(student, semester)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getGradesSummary(semester).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteGradesSummary(old.uniqueSubtract(new))
local.saveGradesSummary(new.uniqueSubtract(old))
}
}.flatMap { local.getGradesSummary(semester).toSingle(emptyList()) })
}
}

View File

@ -21,7 +21,6 @@ import io.github.wulkanowy.services.sync.works.AttendanceWork
import io.github.wulkanowy.services.sync.works.CompletedLessonWork
import io.github.wulkanowy.services.sync.works.ExamWork
import io.github.wulkanowy.services.sync.works.GradeStatisticsWork
import io.github.wulkanowy.services.sync.works.GradeSummaryWork
import io.github.wulkanowy.services.sync.works.GradeWork
import io.github.wulkanowy.services.sync.works.HomeworkWork
import io.github.wulkanowy.services.sync.works.LuckyNumberWork
@ -64,10 +63,6 @@ abstract class ServicesModule {
@IntoSet
abstract fun provideAttendanceWork(work: AttendanceWork): Work
@Binds
@IntoSet
abstract fun provideGradeSummaryWork(work: GradeSummaryWork): Work
@Binds
@IntoSet
abstract fun provideExamWork(work: ExamWork): Work

View File

@ -1,14 +0,0 @@
package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.gradessummary.GradeSummaryRepository
import io.reactivex.Completable
import javax.inject.Inject
class GradeSummaryWork @Inject constructor(private val gradeSummaryRepository: GradeSummaryRepository) : Work {
override fun create(student: Student, semester: Semester): Completable {
return gradeSummaryRepository.getGradesSummary(student, semester, true).ignoreElement()
}
}

View File

@ -58,4 +58,3 @@ class GradeWork @Inject constructor(
)
}
}

View File

@ -3,71 +3,76 @@ package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.grade.GradeRepository
import io.github.wulkanowy.data.repositories.gradessummary.GradeSummaryRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.changeModifier
import io.reactivex.Maybe
import io.reactivex.Single
import javax.inject.Inject
class GradeAverageProvider @Inject constructor(
private val preferencesRepository: PreferencesRepository,
private val semesterRepository: SemesterRepository,
private val gradeRepository: GradeRepository,
private val gradeSummaryRepository: GradeSummaryRepository
private val preferencesRepository: PreferencesRepository
) {
private val plusModifier = preferencesRepository.gradePlusModifier
private val minusModifier = preferencesRepository.gradeMinusModifier
fun getGradeAverage(student: Student, semesters: List<Semester>, selectedSemesterId: Int, forceRefresh: Boolean): Single<List<Triple<String, Double, String>>> {
return when (preferencesRepository.gradeAverageMode) {
"all_year" -> getAllYearAverage(student, semesters, selectedSemesterId, forceRefresh)
"only_one_semester" -> getOnlyOneSemesterAverage(student, semesters, selectedSemesterId, forceRefresh)
fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean = false): Single<List<GradeDetailsWithAverage>> {
return semesterRepository.getSemesters(student).flatMap { semesters ->
when (preferencesRepository.gradeAverageMode) {
"only_one_semester" -> getSemesterDetailsWithAverage(student, semesters.single { it.semesterId == semesterId }, forceRefresh)
"all_year" -> calculateWholeYearAverage(student, semesters, semesterId, forceRefresh)
else -> throw IllegalArgumentException("Incorrect grade average mode: ${preferencesRepository.gradeAverageMode} ")
}
}
}
private fun getAllYearAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<List<Triple<String, Double, String>>> {
private fun calculateWholeYearAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<List<GradeDetailsWithAverage>> {
val selectedSemester = semesters.single { it.semesterId == semesterId }
val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 }
return getAverageFromGradeSummary(student, selectedSemester, forceRefresh)
.switchIfEmpty(gradeRepository.getGrades(student, selectedSemester, forceRefresh)
.flatMap { firstGrades ->
if (selectedSemester == firstSemester) Single.just(firstGrades)
else {
gradeRepository.getGrades(student, firstSemester)
.map { secondGrades -> secondGrades + firstGrades }
return getSemesterDetailsWithAverage(student, selectedSemester, forceRefresh).flatMap { selectedDetails ->
val isAnyAverage = selectedDetails.any { it.average != .0 }
if (selectedSemester != firstSemester) {
getSemesterDetailsWithAverage(student, firstSemester, forceRefresh).map { secondDetails ->
selectedDetails.map { selected ->
val second = secondDetails.singleOrNull { it.subject == selected.subject }
selected.copy(
average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) {
(selected.grades + second?.grades.orEmpty()).calcAverage()
} else (selected.average + (second?.average ?: selected.average)) / 2
)
}
}
} else Single.just(selectedDetails)
}
}.map { grades ->
grades.map { if (student.loginMode == Sdk.Mode.SCRAPPER.name) it.changeModifier(plusModifier, minusModifier) else it }
.groupBy { it.subject }
.map { Triple(it.key, it.value.calcAverage(), "") }
})
}
private fun getOnlyOneSemesterAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<List<Triple<String, Double, String>>> {
val selectedSemester = semesters.single { it.semesterId == semesterId }
private fun getSemesterDetailsWithAverage(student: Student, semester: Semester, forceRefresh: Boolean): Single<List<GradeDetailsWithAverage>> {
return gradeRepository.getGrades(student, semester, forceRefresh).map { (details, summaries) ->
val isAnyAverage = summaries.any { it.average != .0 }
val allGrades = details.groupBy { it.subject }
return getAverageFromGradeSummary(student, selectedSemester, forceRefresh)
.switchIfEmpty(gradeRepository.getGrades(student, selectedSemester, forceRefresh)
.map { grades ->
grades.map { if (student.loginMode == Sdk.Mode.SCRAPPER.name) it.changeModifier(plusModifier, minusModifier) else it }
.groupBy { it.subject }
.map { Triple(it.key, it.value.calcAverage(), "") }
})
}
private fun getAverageFromGradeSummary(student: Student, selectedSemester: Semester, forceRefresh: Boolean): Maybe<List<Triple<String, Double, String>>> {
return gradeSummaryRepository.getGradesSummary(student, selectedSemester, forceRefresh)
.toMaybe()
.flatMap {
if (it.any { summary -> summary.average != .0 }) {
Maybe.just(it.map { summary -> Triple(summary.subject, summary.average, summary.pointsSum) })
} else Maybe.empty()
}.filter { !preferencesRepository.gradeAverageForceCalc }
summaries.map { summary ->
val grades = allGrades[summary.subject].orEmpty()
GradeDetailsWithAverage(
subject = summary.subject,
average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) {
grades.map {
if (student.loginMode == Sdk.Mode.SCRAPPER.name) it.changeModifier(plusModifier, minusModifier)
else it
}.calcAverage()
} else summary.average,
points = summary.pointsSum,
summary = summary,
grades = grades
)
}
}
}
}

View File

@ -0,0 +1,12 @@
package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
data class GradeDetailsWithAverage(
val subject: String,
val average: Double,
val points: String,
val summary: GradeSummary,
val grades: List<Grade>
)

View File

@ -107,7 +107,7 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
gradeHeaderAverage.text = formatAverage(header.average, root.context.resources)
gradeHeaderPointsSum.text = root.context.getString(R.string.grade_points_sum, header.pointsSum)
gradeHeaderPointsSum.visibility = if (!header.pointsSum.isNullOrEmpty()) View.VISIBLE else View.GONE
gradeHeaderNumber.text = root.context.resources.getQuantityString(R.plurals.grade_number_item, header.number, header.number)
gradeHeaderNumber.text = root.context.resources.getQuantityString(R.plurals.grade_number_item, header.grades.size, header.grades.size)
gradeHeaderNote.visibility = if (header.newGrades > 0) View.VISIBLE else View.GONE
if (header.newGrades > 0) gradeHeaderNote.text = header.newGrades.toString(10)

View File

@ -12,7 +12,6 @@ data class GradeDetailsItem(
data class GradeDetailsHeader(
val subject: String,
val number: Int,
val average: Double?,
val pointsSum: String?,
var newGrades: Int,

View File

@ -8,6 +8,7 @@ import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider
import io.github.wulkanowy.ui.modules.grade.GradeDetailsWithAverage
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import timber.log.Timber
@ -126,14 +127,7 @@ class GradeDetailsPresenter @Inject constructor(
private fun loadData(semesterId: Int, forceRefresh: Boolean) {
Timber.i("Loading grade details data started")
disposable.add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getSemesters(it).map { semester -> it to semester } }
.flatMap { (student, semesters) ->
averageProvider.getGradeAverage(student, semesters, semesterId, forceRefresh).flatMap { averages ->
gradeRepository.getGrades(student, semesters.first { it.semesterId == semesterId }, forceRefresh)
.map { it.sortedByDescending { grade -> grade.date } }
.map { createGradeItems(it, averages) }
}
}
.flatMap { averageProvider.getGradesDetailsWithAverage(it, semesterId, forceRefresh) }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
@ -146,16 +140,14 @@ class GradeDetailsPresenter @Inject constructor(
}
.subscribe({ grades ->
Timber.i("Loading grade details result: Success")
newGradesAmount = grades
.filter { it.viewType == ViewType.HEADER }
.sumBy { item -> (item.value as GradeDetailsHeader).newGrades }
newGradesAmount = grades.sumBy { it.grades.sumBy { grade -> if (!grade.isRead) 1 else 0 } }
updateMarkAsDoneButton()
view?.run {
showEmpty(grades.isEmpty())
showErrorView(false)
showContent(grades.isNotEmpty())
updateData(
data = grades,
data = createGradeItems(grades),
isGradeExpandable = preferencesRepository.isGradeExpandable,
gradeColorTheme = preferencesRepository.gradeColorTheme
)
@ -178,17 +170,16 @@ class GradeDetailsPresenter @Inject constructor(
}
}
private fun createGradeItems(items: List<Grade>, averages: List<Triple<String, Double, String>>): List<GradeDetailsItem> {
return items.groupBy { grade -> grade.subject }.toSortedMap().map { (subject, grades) ->
private fun createGradeItems(items: List<GradeDetailsWithAverage>): List<GradeDetailsItem> {
return items.filter { it.grades.isNotEmpty() }.map { (subject, average, points, _, grades) ->
val subItems = grades.map {
GradeDetailsItem(it, ViewType.ITEM)
}
listOf(GradeDetailsItem(GradeDetailsHeader(
subject = subject,
average = averages.singleOrNull { subject == it.first }?.second,
pointsSum = averages.singleOrNull { subject == it.first }?.third,
number = grades.size,
average = average,
pointsSum = points,
newGrades = grades.filter { grade -> !grade.isRead }.size,
grades = subItems
), ViewType.HEADER)) + if (preferencesRepository.isGradeExpandable) emptyList() else subItems

View File

@ -1,12 +1,11 @@
package io.github.wulkanowy.ui.modules.grade.summary
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.repositories.gradessummary.GradeSummaryRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider
import io.github.wulkanowy.ui.modules.grade.GradeDetailsWithAverage
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import timber.log.Timber
@ -16,8 +15,6 @@ class GradeSummaryPresenter @Inject constructor(
schedulers: SchedulersProvider,
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val gradeSummaryRepository: GradeSummaryRepository,
private val semesterRepository: SemesterRepository,
private val averageProvider: GradeAverageProvider,
private val analytics: FirebaseAnalyticsHelper
) : BasePresenter<GradeSummaryView>(errorHandler, studentRepository, schedulers) {
@ -33,15 +30,8 @@ class GradeSummaryPresenter @Inject constructor(
fun onParentViewLoadData(semesterId: Int, forceRefresh: Boolean) {
Timber.i("Loading grade summary data started")
disposable.add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getSemesters(it).map { semesters -> it to semesters } }
.flatMap { (student, semesters) ->
gradeSummaryRepository.getGradesSummary(student, semesters.first { it.semesterId == semesterId }, forceRefresh)
.map { it.sortedBy { subject -> subject.subject } }
.flatMap { gradesSummary ->
averageProvider.getGradeAverage(student, semesters, semesterId, forceRefresh)
.map { averages -> createGradeSummaryItems(gradesSummary, averages) }
}
}
.flatMap { averageProvider.getGradesDetailsWithAverage(it, semesterId, forceRefresh) }
.map { createGradeSummaryItems(it) }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
@ -112,11 +102,9 @@ class GradeSummaryPresenter @Inject constructor(
disposable.clear()
}
private fun createGradeSummaryItems(gradesSummary: List<GradeSummary>, averages: List<Triple<String, Double, String>>): List<GradeSummary> {
return gradesSummary
.filter { !checkEmpty(it, averages) }
.map { gradeSummary ->
gradeSummary.copy(average = averages.singleOrNull { gradeSummary.subject == it.first }?.second ?: .0)
private fun createGradeSummaryItems(items: List<GradeDetailsWithAverage>): List<GradeSummary> {
return items.map {
it.summary.copy(average = it.average)
}
}

View File

@ -3,17 +3,17 @@ package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.grade.GradeRepository
import io.github.wulkanowy.data.repositories.gradessummary.GradeSummaryRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.data.repositories.createSemesterEntity
import io.github.wulkanowy.data.repositories.grade.GradeRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.sdk.Sdk
import io.reactivex.Single
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.of
@ -25,10 +25,10 @@ class GradeAverageProviderTest {
lateinit var preferencesRepository: PreferencesRepository
@Mock
lateinit var gradeRepository: GradeRepository
lateinit var semesterRepository: SemesterRepository
@Mock
lateinit var gradeSummaryRepository: GradeSummaryRepository
lateinit var gradeRepository: GradeRepository
private lateinit var gradeAverageProvider: GradeAverageProvider
@ -41,165 +41,192 @@ class GradeAverageProviderTest {
)
private val firstGrades = listOf(
// avg: 3.5
getGrade(22, "Matematyka", 4.0),
getGrade(22, "Matematyka", 3.0),
// avg: 3.5
getGrade(22, "Fizyka", 6.0),
getGrade(22, "Fizyka", 1.0)
)
private val secondGrade = listOf(
private val firstSummaries = listOf(
getSummary(semesterId = 22, subject = "Matematyka", average = 3.9),
getSummary(semesterId = 22, subject = "Fizyka", average = 3.1)
)
private val secondGrades = listOf(
// avg: 2.5
getGrade(23, "Matematyka", 2.0),
getGrade(23, "Matematyka", 3.0),
// avg: 3.0
getGrade(23, "Fizyka", 4.0),
getGrade(23, "Fizyka", 2.0)
)
private val secondSummaries = listOf(
getSummary(semesterId = 23, subject = "Matematyka", average = 2.9),
getSummary(semesterId = 23, subject = "Fizyka", average = 3.4)
)
private val secondGradeWithModifier = listOf(
// avg: 3.375
getGrade(24, "Język polski", 3.0, -0.50),
getGrade(24, "Język polski", 4.0, 0.25)
)
private val secondSummariesWithModifier = listOf(
getSummary(24, "Język polski", 3.49)
)
@Before
fun initTest() {
fun setUp() {
MockitoAnnotations.initMocks(this)
gradeAverageProvider = GradeAverageProvider(preferencesRepository, gradeRepository, gradeSummaryRepository)
doReturn(.33).`when`(preferencesRepository).gradeMinusModifier
doReturn(.33).`when`(preferencesRepository).gradePlusModifier
doReturn(false).`when`(preferencesRepository).gradeAverageForceCalc
`when`(preferencesRepository.gradeMinusModifier).thenReturn(.33)
`when`(preferencesRepository.gradePlusModifier).thenReturn(.33)
`when`(preferencesRepository.gradeAverageForceCalc).thenReturn(false)
`when`(semesterRepository.getSemesters(student)).thenReturn(Single.just(semesters))
doReturn(Single.just(firstGrades)).`when`(gradeRepository).getGrades(student, semesters[1], true)
doReturn(Single.just(secondGrade)).`when`(gradeRepository).getGrades(student, semesters[2], true)
gradeAverageProvider = GradeAverageProvider(semesterRepository, gradeRepository, preferencesRepository)
}
@Test
fun onlyOneSemesterTest() {
doReturn("only_one_semester").`when`(preferencesRepository).gradeAverageMode
doReturn(Single.just(emptyList<GradeSummary>())).`when`(gradeSummaryRepository).getGradesSummary(student, semesters[2], true)
`when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true)
`when`(preferencesRepository.gradeAverageMode).thenReturn("only_one_semester")
`when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGrades to secondSummaries))
val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true)
.blockingGet()
val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet()
assertEquals(2, averages.size)
assertEquals(2.5, averages.single { it.first == "Matematyka" }.second, .0)
assertEquals(3.0, averages.single { it.first == "Fizyka" }.second, .0)
assertEquals(2, items.size)
assertEquals(2.5, items.single { it.subject == "Matematyka" }.average, .0)
assertEquals(3.0, items.single { it.subject == "Fizyka" }.average, .0)
}
@Test
fun onlyOneSemester_gradesWithModifiers_default() {
doReturn("only_one_semester").`when`(preferencesRepository).gradeAverageMode
doReturn(Single.just(emptyList<GradeSummary>())).`when`(gradeSummaryRepository).getGradesSummary(student, semesters[2], true)
doReturn(Single.just(secondGradeWithModifier)).`when`(gradeRepository).getGrades(student, semesters[2], true)
`when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true)
`when`(preferencesRepository.gradeAverageMode).thenReturn("only_one_semester")
`when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGradeWithModifier to secondSummariesWithModifier))
val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true)
.blockingGet()
val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet()
assertEquals(3.5, averages.single { it.first == "Język polski" }.second, .0)
assertEquals(3.5, items.single { it.subject == "Język polski" }.average, .0)
}
@Test
fun onlyOneSemester_gradesWithModifiers_api() {
doReturn("only_one_semester").`when`(preferencesRepository).gradeAverageMode
doReturn(Single.just(emptyList<GradeSummary>())).`when`(gradeSummaryRepository).getGradesSummary(student.copy(loginMode = Sdk.Mode.API.name), semesters[2], true)
doReturn(Single.just(secondGradeWithModifier)).`when`(gradeRepository).getGrades(student.copy(loginMode = Sdk.Mode.API.name), semesters[2], true)
val student = student.copy(loginMode = Sdk.Mode.API.name)
val averages = gradeAverageProvider.getGradeAverage(student.copy(loginMode = Sdk.Mode.API.name), semesters, semesters[2].semesterId, true)
.blockingGet()
`when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true)
`when`(preferencesRepository.gradeAverageMode).thenReturn("only_one_semester")
`when`(semesterRepository.getSemesters(student)).thenReturn(Single.just(semesters))
`when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGradeWithModifier to secondSummariesWithModifier))
assertEquals(3.375, averages.single { it.first == "Język polski" }.second, .0)
val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet()
assertEquals(3.375, items.single { it.subject == "Język polski" }.average, .0)
}
@Test
fun onlyOneSemester_gradesWithModifiers_scrapper() {
doReturn("only_one_semester").`when`(preferencesRepository).gradeAverageMode
doReturn(Single.just(emptyList<GradeSummary>())).`when`(gradeSummaryRepository).getGradesSummary(student, semesters[2], true)
doReturn(Single.just(secondGradeWithModifier)).`when`(gradeRepository).getGrades(student.copy(loginMode = Sdk.Mode.SCRAPPER.name), semesters[2], true)
val student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name)
val averages = gradeAverageProvider.getGradeAverage(student.copy(loginMode = Sdk.Mode.SCRAPPER.name), semesters, semesters[2].semesterId, true)
.blockingGet()
`when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true)
`when`(preferencesRepository.gradeAverageMode).thenReturn("only_one_semester")
`when`(semesterRepository.getSemesters(student)).thenReturn(Single.just(semesters))
`when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGradeWithModifier to secondSummariesWithModifier))
assertEquals(3.5, averages.single { it.first == "Język polski" }.second, .0)
val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet()
assertEquals(3.5, items.single { it.subject == "Język polski" }.average, .0)
}
@Test
fun onlyOneSemester_gradesWithModifiers_hybrid() {
doReturn("only_one_semester").`when`(preferencesRepository).gradeAverageMode
doReturn(Single.just(emptyList<GradeSummary>())).`when`(gradeSummaryRepository).getGradesSummary(student.copy(loginMode = Sdk.Mode.HYBRID.name), semesters[2], true)
doReturn(Single.just(secondGradeWithModifier)).`when`(gradeRepository).getGrades(student.copy(loginMode = Sdk.Mode.HYBRID.name), semesters[2], true)
val student = student.copy(loginMode = Sdk.Mode.HYBRID.name)
val averages = gradeAverageProvider.getGradeAverage(student.copy(loginMode = Sdk.Mode.HYBRID.name), semesters, semesters[2].semesterId, true)
.blockingGet()
`when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true)
`when`(preferencesRepository.gradeAverageMode).thenReturn("only_one_semester")
`when`(semesterRepository.getSemesters(student)).thenReturn(Single.just(semesters))
`when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGradeWithModifier to secondSummariesWithModifier))
assertEquals(3.375, averages.single { it.first == "Język polski" }.second, .0)
val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet()
assertEquals(3.375, items.single { it.subject == "Język polski" }.average, .0)
}
@Test
fun allYearFirstSemesterTest() {
doReturn("all_year").`when`(preferencesRepository).gradeAverageMode
doReturn(Single.just(emptyList<GradeSummary>())).`when`(gradeSummaryRepository).getGradesSummary(student, semesters[1], true)
`when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true)
`when`(preferencesRepository.gradeAverageMode).thenReturn("all_year")
`when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(firstGrades to firstSummaries))
val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[1].semesterId, true)
.blockingGet()
val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[1].semesterId).blockingGet()
assertEquals(2, averages.size)
assertEquals(3.5, averages.single { it.first == "Matematyka" }.second, .0)
assertEquals(3.5, averages.single { it.first == "Fizyka" }.second, .0)
assertEquals(2, items.size)
assertEquals(3.5, items.single { it.subject == "Matematyka" }.average, .0)
assertEquals(3.5, items.single { it.subject == "Fizyka" }.average, .0)
}
@Test
fun allYearSecondSemesterTest() {
doReturn("all_year").`when`(preferencesRepository).gradeAverageMode
doReturn(Single.just(firstGrades)).`when`(gradeRepository).getGrades(student, semesters[1], false)
doReturn(Single.just(emptyList<GradeSummary>())).`when`(gradeSummaryRepository).getGradesSummary(student, semesters[2], true)
`when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true)
`when`(preferencesRepository.gradeAverageMode).thenReturn("all_year")
`when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(firstGrades to firstSummaries))
`when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGrades to secondSummaries))
val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true)
.blockingGet()
val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet()
assertEquals(2, averages.size)
assertEquals(3.0, averages.single { it.first == "Matematyka" }.second, .0)
assertEquals(3.25, averages.single { it.first == "Fizyka" }.second, .0)
assertEquals(2, items.size)
assertEquals(3.0, items.single { it.subject == "Matematyka" }.average, .0)
assertEquals(3.25, items.single { it.subject == "Fizyka" }.average, .0)
}
@Test(expected = IllegalArgumentException::class)
fun incorrectAverageModeTest() {
doReturn("test_mode").`when`(preferencesRepository).gradeAverageMode
`when`(preferencesRepository.gradeAverageMode).thenReturn("test_mode")
gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true).blockingGet()
gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).blockingGet()
}
@Test
fun onlyOneSemester_averageFromSummary() {
doReturn("all_year").`when`(preferencesRepository).gradeAverageMode
doReturn(Single.just(firstGrades)).`when`(gradeRepository).getGrades(student, semesters[1], false)
doReturn(Single.just(listOf(
getSummary(22, "Matematyka", 3.1),
getSummary(22, "Fizyka", 3.26)
))).`when`(gradeSummaryRepository).getGradesSummary(student, semesters[2], true)
fun allYearSemester_averageFromSummary() {
`when`(preferencesRepository.gradeAverageMode).thenReturn("all_year")
`when`(preferencesRepository.gradeAverageForceCalc).thenReturn(false)
`when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(firstGrades to listOf(
getSummary(22, "Matematyka", 3.0),
getSummary(22, "Fizyka", 3.5)
)))
`when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGrades to listOf(
getSummary(22, "Matematyka", 3.5),
getSummary(22, "Fizyka", 4.0)
)))
val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true)
.blockingGet()
val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet()
assertEquals(2, averages.size)
assertEquals(3.1, averages.single { it.first == "Matematyka" }.second, .0)
assertEquals(3.26, averages.single { it.first == "Fizyka" }.second, .0)
assertEquals(2, items.size)
assertEquals(3.25, items.single { it.subject == "Matematyka" }.average, .0)
assertEquals(3.75, items.single { it.subject == "Fizyka" }.average, .0)
}
@Test
fun onlyOneSemester_averageFromSummary_forceCalc() {
doReturn(true).`when`(preferencesRepository).gradeAverageForceCalc
doReturn("all_year").`when`(preferencesRepository).gradeAverageMode
doReturn(Single.just(firstGrades)).`when`(gradeRepository).getGrades(student, semesters[1], false)
doReturn(Single.just(listOf(
getSummary(22, "Matematyka", 3.1),
getSummary(22, "Fizyka", 3.26)
))).`when`(gradeSummaryRepository).getGradesSummary(student, semesters[2], true)
`when`(preferencesRepository.gradeAverageMode).thenReturn("all_year")
`when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true)
`when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(firstGrades to firstSummaries))
`when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGrades to listOf(
getSummary(22, "Matematyka", 1.1),
getSummary(22, "Fizyka", 7.26)
)))
val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true)
.blockingGet()
val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet()
assertEquals(2, averages.size)
assertEquals(3.0, averages.single { it.first == "Matematyka" }.second, .0)
assertEquals(3.25, averages.single { it.first == "Fizyka" }.second, .0)
assertEquals(2, items.size)
assertEquals(3.0, items.single { it.subject == "Matematyka" }.average, .0)
assertEquals(3.25, items.single { it.subject == "Fizyka" }.average, .0)
}
private fun getGrade(semesterId: Int, subject: String, value: Double, modifier: Double = 0.0): Grade {
@ -221,12 +248,12 @@ class GradeAverageProviderTest {
)
}
private fun getSummary(semesterId: Int, subject: String, value: Double): GradeSummary {
private fun getSummary(semesterId: Int, subject: String, average: Double): GradeSummary {
return GradeSummary(
studentId = 101,
semesterId = semesterId,
subject = subject,
average = value,
average = average,
pointsSum = "",
proposedPoints = "",
finalPoints = "",