Add counting of the full-year average to the summary of grades (#322)

This commit is contained in:
Rafał Borcz 2019-04-18 00:32:43 +02:00 committed by Mikołaj Pich
parent 74e98e4430
commit 034b99c7ab
11 changed files with 275 additions and 71 deletions

View File

@ -17,6 +17,9 @@ class PreferencesRepository @Inject constructor(
val isShowPresent: Boolean
get() = sharedPref.getBoolean(context.getString(R.string.pref_key_attendance_present), true)
val gradeAverageMode: String
get() = sharedPref.getString(context.getString(R.string.pref_key_grade_average_mode), "only_one_semester") ?: "only_one_semester"
val isGradeExpandable: Boolean
get() = !sharedPref.getBoolean(context.getString(R.string.pref_key_expand_grade), false)
@ -50,8 +53,7 @@ class PreferencesRepository @Inject constructor(
get() = sharedPref.getString(context.getString(R.string.pref_key_grade_modifier_plus), "0.0")?.toDouble() ?: 0.0
val gradeMinusModifier: Double
get() = sharedPref.getString(context.getString(R.string.pref_key_grade_modifier_minus), "0.0")?.toDouble()
?: 0.0
get() = sharedPref.getString(context.getString(R.string.pref_key_grade_modifier_minus), "0.0")?.toDouble() ?: 0.0
val fillMessageContent: Boolean
get() = sharedPref.getBoolean(context.getString(R.string.pref_key_fill_message_content), true)

View File

@ -0,0 +1,54 @@
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.preferences.PreferencesRepository
import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.changeModifier
import io.reactivex.Single
import javax.inject.Inject
class GradeAverageProvider @Inject constructor(
private val preferencesRepository: PreferencesRepository,
private val gradeRepository: GradeRepository
) {
fun getGradeAverage(student: Student, semesters: List<Semester>, selectedSemesterId: Int, forceRefresh: Boolean): Single<Map<String, Double>> {
return when (preferencesRepository.gradeAverageMode) {
"all_year" -> getAllYearAverage(student, semesters, selectedSemesterId, forceRefresh)
"only_one_semester" -> getOnlyOneSemesterAverage(student, semesters, selectedSemesterId, forceRefresh)
else -> throw IllegalArgumentException("Incorrect grade average mode: ${preferencesRepository.gradeAverageMode} ")
}
}
private fun getAllYearAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<Map<String, Double>> {
val selectedSemester = semesters.single { it.semesterId == semesterId }
val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 }
val plusModifier = preferencesRepository.gradePlusModifier
val minusModifier = preferencesRepository.gradeMinusModifier
return gradeRepository.getGrades(student, selectedSemester, forceRefresh)
.flatMap { firstGrades ->
if (selectedSemester == firstSemester) Single.just(firstGrades)
else gradeRepository.getGrades(student, firstSemester)
.map { secondGrades -> secondGrades + firstGrades }
}.map { grades ->
grades.map { it.changeModifier(plusModifier, minusModifier) }
.groupBy { it.subject }
.mapValues { it.value.calcAverage() }
}
}
private fun getOnlyOneSemesterAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<Map<String, Double>> {
val selectedSemester = semesters.single { it.semesterId == semesterId }
val plusModifier = preferencesRepository.gradePlusModifier
val minusModifier = preferencesRepository.gradeMinusModifier
return gradeRepository.getGrades(student, selectedSemester, forceRefresh)
.map { grades ->
grades.map { it.changeModifier(plusModifier, minusModifier) }
.groupBy { it.subject }
.mapValues { it.value.calcAverage() }
}
}
}

View File

@ -8,10 +8,9 @@ import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.session.BaseSessionPresenter
import io.github.wulkanowy.ui.base.session.SessionErrorHandler
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.changeModifier
import io.github.wulkanowy.utils.getBackgroundColor
import timber.log.Timber
import javax.inject.Inject
@ -23,6 +22,7 @@ class GradeDetailsPresenter @Inject constructor(
private val studentRepository: StudentRepository,
private val semesterRepository: SemesterRepository,
private val preferencesRepository: PreferencesRepository,
private val averageProvider: GradeAverageProvider,
private val analytics: FirebaseAnalyticsHelper
) : BaseSessionPresenter<GradeDetailsView>(errorHandler) {
@ -109,11 +109,16 @@ 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 -> semester to it } }
.flatMap { gradeRepository.getGrades(it.second, it.first.first { item -> item.semesterId == semesterId }, forceRefresh) }
.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 { semester -> semester.semesterId == semesterId })
.map { it.sortedByDescending { grade -> grade.date } }
.map { it.map { item -> item.changeModifier(preferencesRepository.gradePlusModifier, preferencesRepository.gradeMinusModifier) } }
.map { createGradeItems(it.groupBy { grade -> grade.subject }.toSortedMap()) }
.map { it.groupBy { grade -> grade.subject }.toSortedMap() }
.map { createGradeItems(it, averages) }
}
}
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
@ -139,32 +144,36 @@ class GradeDetailsPresenter @Inject constructor(
})
}
private fun createGradeItems(items: Map<String, List<Grade>>): List<GradeDetailsHeader> {
private fun createGradeItems(items: Map<String, List<Grade>>, averages: Map<String, Double>): List<GradeDetailsHeader> {
val isGradeExpandable = preferencesRepository.isGradeExpandable
val gradeColorTheme = preferencesRepository.gradeColorTheme
val noDescriptionString = view?.noDescriptionString.orEmpty()
val weightString = view?.weightString.orEmpty()
return items.map {
it.value.calcAverage().let { average ->
GradeDetailsHeader(
subject = it.key,
average = formatAverage(average),
average = formatAverage(averages[it.key]),
number = view?.getGradeNumberString(it.value.size).orEmpty(),
newGrades = it.value.filter { grade -> !grade.isRead }.size,
isExpandable = preferencesRepository.isGradeExpandable
isExpandable = isGradeExpandable
).apply {
subItems = it.value.map { item ->
GradeDetailsItem(
grade = item,
valueBgColor = item.getBackgroundColor(preferencesRepository.gradeColorTheme),
weightString = view?.weightString.orEmpty(),
noDescriptionString = view?.noDescriptionString.orEmpty()
valueBgColor = item.getBackgroundColor(gradeColorTheme),
weightString = weightString,
noDescriptionString = noDescriptionString
)
}
}
}
}
}
private fun formatAverage(average: Double): String {
private fun formatAverage(average: Double?): String {
return view?.run {
if (average == 0.0) emptyAverageString
if (average == null || average == .0) emptyAverageString
else averageString.format(average)
}.orEmpty()
}

View File

@ -1,17 +1,15 @@
package io.github.wulkanowy.ui.modules.grade.summary
import io.github.wulkanowy.data.db.entities.GradeSummary
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.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.session.BaseSessionPresenter
import io.github.wulkanowy.ui.base.session.SessionErrorHandler
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.changeModifier
import timber.log.Timber
import java.lang.String.format
import java.util.Locale.FRANCE
@ -20,10 +18,9 @@ import javax.inject.Inject
class GradeSummaryPresenter @Inject constructor(
private val errorHandler: SessionErrorHandler,
private val gradeSummaryRepository: GradeSummaryRepository,
private val gradeRepository: GradeRepository,
private val studentRepository: StudentRepository,
private val semesterRepository: SemesterRepository,
private val preferencesRepository: PreferencesRepository,
private val averageProvider: GradeAverageProvider,
private val schedulers: SchedulersProvider,
private val analytics: FirebaseAnalyticsHelper
) : BaseSessionPresenter<GradeSummaryView>(errorHandler) {
@ -36,25 +33,12 @@ 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 { semester -> semester to it } }
.map { pair -> pair.first.first { it.semesterId == semesterId } to pair.second }
.flatMap {
gradeSummaryRepository.getGradesSummary(it.first, forceRefresh)
.flatMap { semesterRepository.getSemesters(it).map { semesters -> it to semesters } }
.flatMap { (student, semesters) ->
gradeSummaryRepository.getGradesSummary(semesters.first { it.semesterId == semesterId }, forceRefresh)
.flatMap { gradesSummary ->
gradeRepository.getGrades(it.second, it.first, forceRefresh)
.map { grades ->
grades.map { item -> item.changeModifier(preferencesRepository.gradePlusModifier, preferencesRepository.gradeMinusModifier) }
.groupBy { grade -> grade.subject }
.mapValues { entry -> entry.value.calcAverage() }
.filterValues { value -> value != 0.0 }
.let { averages ->
createGradeSummaryItems(gradesSummary, averages) to
GradeSummaryScrollableHeader(
formatAverage(gradesSummary.calcAverage()),
formatAverage(averages.values.average())
)
}
}
averageProvider.getGradeAverage(student, semesters, semesterId, forceRefresh)
.map { averages -> createGradeSummaryItemsAndHeader(gradesSummary, averages) }
}
}
.subscribeOn(schedulers.backgroundThread)
@ -66,14 +50,14 @@ class GradeSummaryPresenter @Inject constructor(
enableSwipe(true)
notifyParentDataLoaded(semesterId)
}
}.subscribe({
}.subscribe({ (gradeSummaryItems, gradeSummaryHeader) ->
Timber.i("Loading grade summary result: Success")
view?.run {
showEmpty(it.first.isEmpty())
showContent(it.first.isNotEmpty())
updateData(it.first, it.second)
showEmpty(gradeSummaryItems.isEmpty())
showContent(gradeSummaryItems.isNotEmpty())
updateData(gradeSummaryItems, gradeSummaryHeader)
}
analytics.logEvent("load_grade_summary", "items" to it.first.size, "force_refresh" to forceRefresh)
analytics.logEvent("load_grade_summary", "items" to gradeSummaryItems.size, "force_refresh" to forceRefresh)
}) {
Timber.i("Loading grade summary result: An exception occurred")
view?.run { showEmpty(isViewEmpty) }
@ -104,15 +88,23 @@ class GradeSummaryPresenter @Inject constructor(
disposable.clear()
}
private fun createGradeSummaryItems(gradesSummary: List<GradeSummary>, averages: Map<String, Double>)
: List<GradeSummaryItem> {
return gradesSummary.filter { !checkEmpty(it, averages) }.map { it ->
private fun createGradeSummaryItemsAndHeader(gradesSummary: List<GradeSummary>, averages: Map<String, Double>)
: Pair<List<GradeSummaryItem>, GradeSummaryScrollableHeader> {
return averages.filterValues { value -> value != 0.0 }
.let { filteredAverages ->
gradesSummary.filter { !checkEmpty(it, filteredAverages) }
.map {
GradeSummaryItem(
title = it.subject,
average = formatAverage(averages.getOrElse(it.subject) { 0.0 }, ""),
average = formatAverage(filteredAverages.getOrElse(it.subject) { 0.0 }, ""),
predictedGrade = it.predictedGrade,
finalGrade = it.finalGrade
)
}.let {
it to GradeSummaryScrollableHeader(
formatAverage(gradesSummary.calcAverage()),
formatAverage(filteredAverages.values.average()))
}
}
}

View File

@ -239,11 +239,11 @@
<!--Preferences-->
<string name="pref_view_header">Wygląd</string>
<string name="pref_view_list">Domyślny widok</string>
<string name="pref_view_summary">Pokazuj podsumowanie w ocenach</string>
<string name="pref_view_grade_average_mode">Obliczanie średniej końcoworocznej</string>
<string name="pref_view_present">Pokazuj obecność we frekwencji</string>
<string name="pref_view_theme_dark">Ciemny motyw (Beta)</string>
<string name="pref_view_expand_grade">Rozwiń oceny</string>
<string name="pref_grade_color_scheme">Schemat kolorów ocen</string>
<string name="pref_view_grade_color_scheme">Schemat kolorów ocen</string>
<string name="pref_notify_header">Powiadomienia</string>
<string name="pref_notify_switch">Pokazuj powiadomienia</string>

View File

@ -30,4 +30,9 @@
<item>Wulkanowy</item>
<item>Kolory ocen w dzienniku</item>
</string-array>
<string-array name="grade_average_mode_entries">
<item>Średnia ocen z 2 semestru</item>
<item>Średnia ocen z całego roku</item>
</string-array>
</resources>

View File

@ -5,6 +5,7 @@
<string name="pref_key_theme">theme</string>
<string name="pref_key_grade_color_scheme">grade_color_scheme</string>
<string name="pref_key_expand_grade">expand_grade</string>
<string name="pref_key_grade_average_mode">grade_average_mode</string>
<string name="pref_key_services_enable">services_enable</string>
<string name="pref_key_services_interval">services_interval</string>
<string name="pref_key_services_wifi_only">services_disable_wifi_only</string>

View File

@ -224,11 +224,11 @@
<!--Preferences-->
<string name="pref_view_header">Appearance</string>
<string name="pref_view_list">Default view</string>
<string name="pref_view_summary">Show the summary in the grades</string>
<string name="pref_view_grade_average_mode">Calculation of the end-of-year average</string>
<string name="pref_view_present">Show presence in attendance</string>
<string name="pref_view_theme_dark">Dark theme (Beta)</string>
<string name="pref_view_expand_grade">Expand grades</string>
<string name="pref_grade_color_scheme">Grades color scheme</string>
<string name="pref_view_grade_color_scheme">Grades color scheme</string>
<string name="pref_notify_header">Notifications</string>
<string name="pref_notify_switch">Show notifications</string>

View File

@ -70,4 +70,13 @@
<item>material</item>
<item>grade_color</item>
</string-array>
<string-array name="grade_average_mode_entries">
<item>Average grades from the 2nd semester</item>
<item>Average of grades from the whole year</item>
</string-array>
<string-array name="grade_average_mode_values" translatable="false">
<item>only_one_semester</item>
<item>all_year</item>
</string-array>
</resources>

View File

@ -36,7 +36,7 @@
android:entryValues="@array/grade_color_scheme_values"
android:key="@string/pref_key_grade_color_scheme"
android:summary="%s"
android:title="@string/pref_grade_color_scheme"
android:title="@string/pref_view_grade_color_scheme"
app:iconSpaceReserved="false" />
</PreferenceCategory>
<PreferenceCategory
@ -97,6 +97,14 @@
android:summary="%s"
android:title="@string/pref_other_grade_modifier_minus"
app:iconSpaceReserved="false" />
<ListPreference
android:defaultValue="only_one_semester"
android:entries="@array/grade_average_mode_entries"
android:entryValues="@array/grade_average_mode_values"
android:key="@string/pref_key_grade_average_mode"
android:summary="%s"
android:title="@string/pref_view_grade_average_mode"
app:iconSpaceReserved="false" />
<SwitchPreference
android:defaultValue="true"
android:key="@string/pref_key_fill_message_content"

View File

@ -0,0 +1,124 @@
package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.db.entities.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.preferences.PreferencesRepository
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.MockitoAnnotations
import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDateTime
class GradeAverageProviderTest {
@Mock
lateinit var preferencesRepository: PreferencesRepository
@Mock
lateinit var gradeRepository: GradeRepository
private lateinit var gradeAverageProvider: GradeAverageProvider
private val student = Student("", "", "", "", "", 101, "", "", "", "", 1, true, LocalDateTime.now())
private val semesters = mutableListOf(
Semester(101, 10, "", 1, 21, 1, false, now(), now(), 1, 1),
Semester(101, 11, "", 1, 22, 1, false, now(), now(), 1, 1),
Semester(101, 11, "", 1, 23, 2, true, now(), now(), 1, 1)
)
private val firstGrades = listOf(
getGrade(101, 22, "Matematyka", 4, .0, 1.0),
getGrade(101, 22, "Matematyka", 3, .0, 1.0),
getGrade(101, 22, "Fizyka", 6, .0, 1.0),
getGrade(101, 22, "Fizyka", 1, .0, 1.0)
)
private val secondGrade = listOf(
getGrade(101, 23, "Matematyka", 2, .0, 1.0),
getGrade(101, 23, "Matematyka", 3, .0, 1.0),
getGrade(101, 23, "Fizyka", 4, .0, 1.0),
getGrade(101, 23, "Fizyka", 2, .0, 1.0)
)
@Before
fun initTest() {
MockitoAnnotations.initMocks(this)
gradeAverageProvider = GradeAverageProvider(preferencesRepository, gradeRepository)
doReturn(.33).`when`(preferencesRepository).gradeMinusModifier
doReturn(.33).`when`(preferencesRepository).gradePlusModifier
doReturn(Single.just(firstGrades)).`when`(gradeRepository).getGrades(student, semesters[1], true)
doReturn(Single.just(secondGrade)).`when`(gradeRepository).getGrades(student, semesters[2], true)
}
@Test
fun onlyOneSemesterTest() {
doReturn("only_one_semester").`when`(preferencesRepository).gradeAverageMode
val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true)
.blockingGet()
assertEquals(2, averages.size)
assertEquals(2.5, averages["Matematyka"])
assertEquals(3.0, averages["Fizyka"])
}
@Test
fun allYearFirstSemesterTest() {
doReturn("all_year").`when`(preferencesRepository).gradeAverageMode
val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[1].semesterId, true)
.blockingGet()
assertEquals(2, averages.size)
assertEquals(3.5, averages["Matematyka"])
assertEquals(3.5, averages["Fizyka"])
}
@Test
fun allYearSecondSemesterTest() {
doReturn("all_year").`when`(preferencesRepository).gradeAverageMode
doReturn(Single.just(firstGrades)).`when`(gradeRepository).getGrades(student, semesters[1], false)
val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true)
.blockingGet()
assertEquals(2, averages.size)
assertEquals(3.0, averages["Matematyka"])
assertEquals(3.25, averages["Fizyka"])
}
@Test(expected = IllegalArgumentException::class)
fun incorrectAverageModeTest() {
doReturn("test_mode").`when`(preferencesRepository).gradeAverageMode
gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true).blockingGet()
}
private fun getGrade(studentId: Int, semesterId: Int, subject: String, value: Int, modifier: Double, weight: Double): Grade {
return Grade(
studentId = studentId,
semesterId = semesterId,
subject = subject,
value = value,
modifier = modifier,
weightValue = weight,
teacher = "",
date = now(),
weight = "",
gradeSymbol = "",
entry = "",
description = "",
comment = "",
color = ""
)
}
}