diff --git a/app/build.gradle b/app/build.gradle
index 66542f78f..4d53481bf 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -97,6 +97,9 @@ dependencies {
implementation "com.aurelhubert:ahbottomnavigation:2.3.4"
implementation 'com.ncapdevi:frag-nav:3.1.0'
+ implementation "com.hootsuite.android:nachos:1.1.1"
+ implementation 'com.github.PhilJay:MPAndroidChart:971640b29d'
+
implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.2'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation "io.reactivex.rxjava2:rxjava:2.2.5"
@@ -125,8 +128,6 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation 'org.mockito:mockito-android:2.23.4'
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
-
- implementation "com.hootsuite.android:nachos:1.1.1"
}
apply plugin: 'com.google.gms.google-services'
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 12360845c..15b628384 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -32,5 +32,8 @@
-dontwarn rx.internal.util.**
-dontwarn sun.misc.Unsafe
+#Config for MPAndroidChart
+-keep class com.github.mikephil.charting.** { *; }
+
#Config for API
-keep class io.github.wulkanowy.api.** {*;}
diff --git a/app/src/androidTest/java/io/github/wulkanowy/data/repositories/grade/GradeStatisticsLocalTest.kt b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/grade/GradeStatisticsLocalTest.kt
new file mode 100644
index 000000000..f85dabf36
--- /dev/null
+++ b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/grade/GradeStatisticsLocalTest.kt
@@ -0,0 +1,69 @@
+package io.github.wulkanowy.data.repositories.grade
+
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.github.wulkanowy.data.db.AppDatabase
+import io.github.wulkanowy.data.db.entities.GradeStatistics
+import io.github.wulkanowy.data.db.entities.Semester
+import io.github.wulkanowy.data.repositories.gradestatistics.GradeStatisticsLocal
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertEquals
+
+@RunWith(AndroidJUnit4::class)
+class GradeStatisticsLocalTest {
+
+ private lateinit var gradeStatisticsLocal: GradeStatisticsLocal
+
+ private lateinit var testDb: AppDatabase
+
+ @Before
+ fun createDb() {
+ testDb = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), AppDatabase::class.java)
+ .build()
+ gradeStatisticsLocal = GradeStatisticsLocal(testDb.gradeStatistics)
+ }
+
+ @After
+ fun closeDb() {
+ testDb.close()
+ }
+
+ @Test
+ fun saveAndRead_subject() {
+ gradeStatisticsLocal.saveGradesStatistics(listOf(
+ getGradeStatistics("Matematyka", 2, 1),
+ getGradeStatistics("Fizyka", 1, 2)
+ ))
+
+ val stats = gradeStatisticsLocal.getGradesStatistics(
+ Semester(2, 2, "", 1, 2, true, 1 ,1), false,
+ "Matematyka"
+ ).blockingGet()
+ assertEquals(1, stats.size)
+ assertEquals(stats[0].subject, "Matematyka")
+ }
+
+ @Test
+ fun saveAndRead_all() {
+ gradeStatisticsLocal.saveGradesStatistics(listOf(
+ getGradeStatistics("Matematyka", 2, 1),
+ getGradeStatistics("Chemia", 2, 1),
+ getGradeStatistics("Fizyka", 1, 2)
+ ))
+
+ val stats = gradeStatisticsLocal.getGradesStatistics(
+ Semester(2, 2, "", 1, 2, true, 1, 1), false,
+ "Wszystkie"
+ ).blockingGet()
+ assertEquals(1, stats.size)
+ assertEquals(stats[0].subject, "Wszystkie")
+ }
+
+ private fun getGradeStatistics(subject: String, studentId: Int, semesterId: Int): GradeStatistics {
+ return GradeStatistics(studentId, semesterId, subject, 5, 5, false)
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2bc7e0a91..cb24e2aa5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,7 +4,7 @@
package="io.github.wulkanowy"
android:installLocation="internalOnly">
-
+
@@ -20,7 +20,7 @@
android:supportsRtl="false"
android:theme="@style/WulkanowyTheme"
android:usesCleartextTraffic="true"
- tools:ignore="GoogleAppIndexingWarning">
+ tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
)
+
+ @Delete
+ fun deleteAll(gradesStatistics: List)
+
+ @Query("SELECT * FROM GradesStatistics WHERE student_id = :studentId AND semester_id = :semesterId AND subject = :subjectName AND is_semester = :isSemester")
+ fun loadSubject(semesterId: Int, studentId: Int, subjectName: String, isSemester: Boolean): Maybe>
+
+ @Query("SELECT * FROM GradesStatistics WHERE student_id = :studentId AND semester_id = :semesterId AND is_semester = :isSemester")
+ fun loadAll(semesterId: Int, studentId: Int, isSemester: Boolean): Maybe>
+}
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeStatistics.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeStatistics.kt
new file mode 100644
index 000000000..8ad8b8b8d
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeStatistics.kt
@@ -0,0 +1,27 @@
+package io.github.wulkanowy.data.db.entities
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "GradesStatistics")
+data class GradeStatistics(
+
+ @ColumnInfo(name = "student_id")
+ val studentId: Int,
+
+ @ColumnInfo(name = "semester_id")
+ val semesterId: Int,
+
+ val subject: String,
+
+ val grade: Int,
+
+ val amount: Int,
+
+ @ColumnInfo(name = "is_semester")
+ val semester: Boolean
+) {
+ @PrimaryKey(autoGenerate = true)
+ var id: Long = 0
+}
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration7.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration7.kt
new file mode 100644
index 000000000..683051244
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration7.kt
@@ -0,0 +1,18 @@
+package io.github.wulkanowy.data.db.migrations
+
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+
+class Migration7 : Migration(6, 7) {
+
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("CREATE TABLE IF NOT EXISTS `GradesStatistics` (" +
+ "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
+ "`student_id` INTEGER NOT NULL," +
+ "`semester_id` INTEGER NOT NULL," +
+ "`subject` TEXT NOT NULL," +
+ "`grade` INTEGER NOT NULL," +
+ "`amount` INTEGER NOT NULL," +
+ "`is_semester` INTEGER NOT NULL)")
+ }
+}
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/gradestatistics/GradeStatisticsLocal.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/gradestatistics/GradeStatisticsLocal.kt
new file mode 100644
index 000000000..581ac2f81
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/gradestatistics/GradeStatisticsLocal.kt
@@ -0,0 +1,34 @@
+package io.github.wulkanowy.data.repositories.gradestatistics
+
+import io.github.wulkanowy.data.db.dao.GradeStatisticsDao
+import io.github.wulkanowy.data.db.entities.GradeStatistics
+import io.github.wulkanowy.data.db.entities.Semester
+import io.reactivex.Maybe
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class GradeStatisticsLocal @Inject constructor(private val gradeStatisticsDb: GradeStatisticsDao) {
+
+ fun getGradesStatistics(semester: Semester, isSemester: Boolean): Maybe> {
+ return gradeStatisticsDb.loadAll(semester.semesterId, semester.studentId, isSemester)
+ .filter { !it.isEmpty() }
+ }
+
+ fun getGradesStatistics(semester: Semester, isSemester: Boolean, subjectName: String): Maybe> {
+ return (if ("Wszystkie" == subjectName) gradeStatisticsDb.loadAll(semester.semesterId, semester.studentId, isSemester).map { list ->
+ list.groupBy { it.grade }.map {
+ GradeStatistics(semester.studentId, semester.semesterId, subjectName, it.key, it.value.fold(0) { acc, e -> acc + e.amount }, false)
+ }
+ }
+ else gradeStatisticsDb.loadSubject(semester.semesterId, semester.studentId, subjectName, isSemester)).filter { !it.isEmpty() }
+ }
+
+ fun saveGradesStatistics(gradesStatistics: List) {
+ gradeStatisticsDb.insertAll(gradesStatistics)
+ }
+
+ fun deleteGradesStatistics(gradesStatistics: List) {
+ gradeStatisticsDb.deleteAll(gradesStatistics)
+ }
+}
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/gradestatistics/GradeStatisticsRemote.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/gradestatistics/GradeStatisticsRemote.kt
new file mode 100644
index 000000000..fa3b951f6
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/gradestatistics/GradeStatisticsRemote.kt
@@ -0,0 +1,29 @@
+package io.github.wulkanowy.data.repositories.gradestatistics
+
+import io.github.wulkanowy.api.Api
+import io.github.wulkanowy.data.db.entities.GradeStatistics
+import io.github.wulkanowy.data.db.entities.Semester
+import io.reactivex.Single
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class GradeStatisticsRemote @Inject constructor(private val api: Api) {
+
+ fun getGradeStatistics(semester: Semester, isSemester: Boolean): Single> {
+ return Single.just(api.apply { diaryId = semester.diaryId })
+ .flatMap { it.getGradesStatistics(semester.semesterId, isSemester) }
+ .map { gradeStatistics ->
+ gradeStatistics.map {
+ GradeStatistics(
+ semesterId = semester.semesterId,
+ studentId = semester.studentId,
+ subject = it.subject,
+ grade = it.gradeValue,
+ amount = it.amount ?: 0,
+ semester = isSemester
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/gradestatistics/GradeStatisticsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/gradestatistics/GradeStatisticsRepository.kt
new file mode 100644
index 000000000..870bd1e93
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/gradestatistics/GradeStatisticsRepository.kt
@@ -0,0 +1,33 @@
+package io.github.wulkanowy.data.repositories.gradestatistics
+
+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.GradeStatistics
+import io.github.wulkanowy.data.db.entities.Semester
+import io.reactivex.Single
+import java.net.UnknownHostException
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class GradeStatisticsRepository @Inject constructor(
+ private val settings: InternetObservingSettings,
+ private val local: GradeStatisticsLocal,
+ private val remote: GradeStatisticsRemote
+) {
+
+ fun getGradesStatistics(semester: Semester, subjectName: String, isSemester: Boolean, forceRefresh: Boolean = false): Single> {
+ return local.getGradesStatistics(semester, isSemester, subjectName).filter { !forceRefresh }
+ .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
+ .flatMap {
+ if (it) remote.getGradeStatistics(semester, isSemester)
+ else Single.error(UnknownHostException())
+ }.flatMap { newGradesStats ->
+ local.getGradesStatistics(semester, isSemester).toSingle(emptyList())
+ .doOnSuccess { oldGradesStats ->
+ local.deleteGradesStatistics(oldGradesStats - newGradesStats)
+ local.saveGradesStatistics(newGradesStats - oldGradesStats)
+ }
+ }.flatMap { local.getGradesStatistics(semester, isSemester, subjectName).toSingle(emptyList()) })
+ }
+}
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/subject/SubjectRepostory.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/subject/SubjectRepository.kt
similarity index 96%
rename from app/src/main/java/io/github/wulkanowy/data/repositories/subject/SubjectRepostory.kt
rename to app/src/main/java/io/github/wulkanowy/data/repositories/subject/SubjectRepository.kt
index 7571ecf14..6167251b9 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/subject/SubjectRepostory.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/subject/SubjectRepository.kt
@@ -10,7 +10,7 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class SubjectRepostory @Inject constructor(
+class SubjectRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: SubjectLocal,
private val remote: SubjectRemote
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryPresenter.kt
index 0d8d827af..ab9ce1519 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryPresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryPresenter.kt
@@ -5,7 +5,7 @@ import io.github.wulkanowy.data.db.entities.Subject
import io.github.wulkanowy.data.repositories.attendancesummary.AttendanceSummaryRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
-import io.github.wulkanowy.data.repositories.subject.SubjectRepostory
+import io.github.wulkanowy.data.repositories.subject.SubjectRepository
import io.github.wulkanowy.ui.base.session.BaseSessionPresenter
import io.github.wulkanowy.ui.base.session.SessionErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
@@ -21,7 +21,7 @@ import javax.inject.Inject
class AttendanceSummaryPresenter @Inject constructor(
private val errorHandler: SessionErrorHandler,
private val attendanceSummaryRepository: AttendanceSummaryRepository,
- private val subjectRepository: SubjectRepostory,
+ private val subjectRepository: SubjectRepository,
private val studentRepository: StudentRepository,
private val semesterRepository: SemesterRepository,
private val schedulers: SchedulersProvider,
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/CustomTabLayout.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/CustomTabLayout.kt
new file mode 100644
index 000000000..e6f01497c
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/CustomTabLayout.kt
@@ -0,0 +1,34 @@
+package io.github.wulkanowy.ui.modules.grade
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.ViewGroup
+import com.google.android.material.tabs.TabLayout
+
+/**
+ * @see Tabs don't fit to screen with tabmode=scrollable, Even with a Custom Tab Layout
+ */
+class CustomTabLayout : TabLayout {
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
+ val tabLayout = getChildAt(0) as ViewGroup
+ val childCount = tabLayout.childCount
+
+ if (childCount == 0) return
+
+ val tabMinWidth = context.resources.displayMetrics.widthPixels / childCount
+
+ for (i in 0 until childCount) {
+ tabLayout.getChildAt(i).minimumWidth = tabMinWidth
+ }
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt
index 7a1c67ee3..8fdecda2d 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt
@@ -14,6 +14,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
import io.github.wulkanowy.ui.base.session.BaseSessionFragment
import io.github.wulkanowy.ui.modules.grade.details.GradeDetailsFragment
+import io.github.wulkanowy.ui.modules.grade.statistics.GradeStatisticsFragment
import io.github.wulkanowy.ui.modules.grade.summary.GradeSummaryFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.setOnSelectPageListener
@@ -63,12 +64,14 @@ class GradeFragment : BaseSessionFragment(), GradeView, MainView.MainChildView,
containerId = gradeViewPager.id
addFragmentsWithTitle(mapOf(
GradeDetailsFragment.newInstance() to getString(R.string.all_details),
- GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary)
+ GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary),
+ GradeStatisticsFragment.newInstance() to getString(R.string.grade_menu_statistics)
))
}
gradeViewPager.run {
adapter = pagerAdapter
+ offscreenPageLimit = 3
setOnSelectPageListener { presenter.onPageSelected(it) }
}
gradeTabLayout.setupWithViewPager(gradeViewPager)
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeModule.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeModule.kt
index b7b52f218..46a923d9a 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeModule.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeModule.kt
@@ -7,6 +7,7 @@ import io.github.wulkanowy.di.scopes.PerChildFragment
import io.github.wulkanowy.di.scopes.PerFragment
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
import io.github.wulkanowy.ui.modules.grade.details.GradeDetailsFragment
+import io.github.wulkanowy.ui.modules.grade.statistics.GradeStatisticsFragment
import io.github.wulkanowy.ui.modules.grade.summary.GradeSummaryFragment
@Module
@@ -28,4 +29,8 @@ abstract class GradeModule {
@PerChildFragment
@ContributesAndroidInjector
abstract fun binGradeSummaryFragment(): GradeSummaryFragment
+
+ @PerChildFragment
+ @ContributesAndroidInjector
+ abstract fun binGradeStatisticsFragment(): GradeStatisticsFragment
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt
index 7413f056b..cc127a90f 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt
@@ -114,6 +114,6 @@ class GradePresenter @Inject constructor(
}
private fun notifyChildrenSemesterChange() {
- for (i in 0..1) view?.notifyChildSemesterChange(i)
+ for (i in 0..2) view?.notifyChildSemesterChange(i)
}
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt
new file mode 100644
index 000000000..2bd51eac6
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt
@@ -0,0 +1,191 @@
+package io.github.wulkanowy.ui.modules.grade.statistics
+
+import android.graphics.Color.WHITE
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import com.github.mikephil.charting.components.Legend
+import com.github.mikephil.charting.components.LegendEntry
+import com.github.mikephil.charting.data.PieData
+import com.github.mikephil.charting.data.PieDataSet
+import com.github.mikephil.charting.data.PieEntry
+import com.github.mikephil.charting.formatter.ValueFormatter
+import io.github.wulkanowy.R
+import io.github.wulkanowy.data.db.entities.GradeStatistics
+import io.github.wulkanowy.ui.base.session.BaseSessionFragment
+import io.github.wulkanowy.ui.modules.grade.GradeFragment
+import io.github.wulkanowy.ui.modules.grade.GradeView
+import io.github.wulkanowy.utils.getThemeAttrColor
+import io.github.wulkanowy.utils.setOnItemSelectedListener
+import kotlinx.android.synthetic.main.fragment_grade_statistics.*
+import javax.inject.Inject
+
+class GradeStatisticsFragment : BaseSessionFragment(), GradeStatisticsView, GradeView.GradeChildView {
+
+ @Inject
+ lateinit var presenter: GradeStatisticsPresenter
+
+ private lateinit var subjectsAdapter: ArrayAdapter
+
+ companion object {
+ private const val SAVED_CHART_TYPE = "CURRENT_TYPE"
+
+ fun newInstance() = GradeStatisticsFragment()
+ }
+
+ override val isViewEmpty
+ get() = gradeStatisticsChart.isEmpty
+
+ private val gradeColors = listOf(
+ 6 to R.color.grade_material_six,
+ 5 to R.color.grade_material_five,
+ 4 to R.color.grade_material_four,
+ 3 to R.color.grade_material_three,
+ 2 to R.color.grade_material_two,
+ 1 to R.color.grade_material_one
+ )
+
+ private val gradeLabels = listOf(
+ "6, 6-", "5, 5-, 5+", "4, 4-, 4+", "3, 3-, 3+", "2, 2-, 2+", "1, 1+"
+ )
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.fragment_grade_statistics, container, false)
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ messageContainer = gradeStatisticsChart
+ presenter.onAttachView(this, savedInstanceState?.getBoolean(SAVED_CHART_TYPE))
+ }
+
+ override fun initView() {
+ gradeStatisticsChart.run {
+ description.isEnabled = false
+ setHoleColor(context.getThemeAttrColor(android.R.attr.windowBackground))
+ setCenterTextColor(context.getThemeAttrColor(android.R.attr.textColorPrimary))
+ animateXY(1000, 1000)
+ minAngleForSlices = 25f
+ legend.apply {
+ textColor = context.getThemeAttrColor(android.R.attr.textColorPrimary)
+ setCustom(gradeLabels.mapIndexed { i, it ->
+ LegendEntry().apply {
+ label = it
+ formColor = ContextCompat.getColor(context, gradeColors[i].second)
+ form = Legend.LegendForm.SQUARE
+ }
+ })
+ }
+ }
+
+ context?.let {
+ subjectsAdapter = ArrayAdapter(it, android.R.layout.simple_spinner_item, ArrayList())
+ subjectsAdapter.setDropDownViewResource(R.layout.item_attendance_summary_subject)
+ }
+
+ gradeStatisticsSubjects.run {
+ adapter = subjectsAdapter
+ setOnItemSelectedListener { presenter.onSubjectSelected((it as TextView).text.toString()) }
+ }
+
+ gradeStatisticsSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
+ }
+
+ override fun updateSubjects(data: ArrayList) {
+ subjectsAdapter.run {
+ clear()
+ addAll(data)
+ notifyDataSetChanged()
+ }
+ }
+
+ override fun updateData(items: List) {
+ gradeStatisticsChart.run {
+ data = PieData(PieDataSet(items.map {
+ PieEntry(it.amount.toFloat(), it.grade.toString())
+ }, "Legenda").apply {
+ valueTextSize = 12f
+ sliceSpace = 1f
+ valueTextColor = WHITE
+ setColors(items.map {
+ gradeColors.single { color -> color.first == it.grade }.second
+ }.toIntArray(), context)
+ }).apply {
+ setTouchEnabled(false)
+ setValueFormatter(object : ValueFormatter() {
+ override fun getPieLabel(value: Float, pieEntry: PieEntry): String {
+ return resources.getQuantityString(R.plurals.grade_number_item, value.toInt(), value.toInt())
+ }
+ })
+ centerText = items.fold(0) { acc, it -> acc + it.amount }
+ .let { resources.getQuantityString(R.plurals.grade_number_item, it, it) }
+ }
+ invalidate()
+ }
+ }
+
+ override fun showSubjects(show: Boolean) {
+ gradeStatisticsSubjectsContainer.visibility = if (show) View.VISIBLE else View.INVISIBLE
+ }
+
+ override fun clearView() {
+ gradeStatisticsChart.clear()
+ }
+
+ override fun showContent(show: Boolean) {
+ gradeStatisticsChart.visibility = if (show) View.VISIBLE else View.GONE
+ }
+
+ override fun showEmpty(show: Boolean) {
+ gradeStatisticsEmpty.visibility = if (show) View.VISIBLE else View.INVISIBLE
+ }
+
+ override fun showProgress(show: Boolean) {
+ gradeStatisticsProgress.visibility = if (show) View.VISIBLE else View.GONE
+ }
+
+ override fun showRefresh(show: Boolean) {
+ gradeStatisticsSwipe.isRefreshing = show
+ }
+
+ override fun onParentLoadData(semesterId: Int, forceRefresh: Boolean) {
+ presenter.onParentViewLoadData(semesterId, forceRefresh)
+ }
+
+ override fun onParentReselected() {
+ //
+ }
+
+ override fun onParentChangeSemester() {
+ presenter.onParentViewChangeSemester()
+ }
+
+ override fun notifyParentDataLoaded(semesterId: Int) {
+ (parentFragment as? GradeFragment)?.onChildFragmentLoaded(semesterId)
+ }
+
+ override fun notifyParentRefresh() {
+ (parentFragment as? GradeFragment)?.onChildRefresh()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, checkedId ->
+ presenter.onTypeChange(checkedId == R.id.gradeStatisticsTypeSemester)
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putBoolean(GradeStatisticsFragment.SAVED_CHART_TYPE, presenter.currentIsSemester)
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ presenter.onDetachView()
+ }
+}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsPresenter.kt
new file mode 100644
index 000000000..073231c09
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsPresenter.kt
@@ -0,0 +1,140 @@
+package io.github.wulkanowy.ui.modules.grade.statistics
+
+import io.github.wulkanowy.data.db.entities.Subject
+import io.github.wulkanowy.data.repositories.gradestatistics.GradeStatisticsRepository
+import io.github.wulkanowy.data.repositories.semester.SemesterRepository
+import io.github.wulkanowy.data.repositories.student.StudentRepository
+import io.github.wulkanowy.data.repositories.subject.SubjectRepository
+import io.github.wulkanowy.ui.base.session.BaseSessionPresenter
+import io.github.wulkanowy.ui.base.session.SessionErrorHandler
+import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
+import io.github.wulkanowy.utils.SchedulersProvider
+import timber.log.Timber
+import javax.inject.Inject
+
+class GradeStatisticsPresenter @Inject constructor(
+ private val errorHandler: SessionErrorHandler,
+ private val gradeStatisticsRepository: GradeStatisticsRepository,
+ private val subjectRepository: SubjectRepository,
+ private val studentRepository: StudentRepository,
+ private val semesterRepository: SemesterRepository,
+ private val schedulers: SchedulersProvider,
+ private val analytics: FirebaseAnalyticsHelper
+) : BaseSessionPresenter(errorHandler) {
+
+ private var subjects = emptyList()
+
+ private var currentSemesterId = 0
+
+ private var currentSubjectName: String = "Wszystkie"
+
+ var currentIsSemester = false
+ private set
+
+ fun onAttachView(view: GradeStatisticsView, isSemester: Boolean?) {
+ super.onAttachView(view)
+ currentIsSemester = isSemester ?: false
+ view.initView()
+ }
+
+ fun onParentViewLoadData(semesterId: Int, forceRefresh: Boolean) {
+ currentSemesterId = semesterId
+ loadSubjects()
+ loadData(semesterId, currentSubjectName, currentIsSemester, forceRefresh)
+ }
+
+ fun onParentViewChangeSemester() {
+ view?.run {
+ showProgress(true)
+ showRefresh(false)
+ showContent(false)
+ showEmpty(false)
+ clearView()
+ }
+ disposable.clear()
+ }
+
+ fun onSwipeRefresh() {
+ Timber.i("Force refreshing the grade stats")
+ view?.notifyParentRefresh()
+ }
+
+ fun onSubjectSelected(name: String) {
+ Timber.i("Select attendance stats subject $name")
+ view?.run {
+ showContent(false)
+ showProgress(true)
+ showEmpty(false)
+ clearView()
+ }
+ (subjects.singleOrNull { it.name == name }?.name).let {
+ if (it != currentSubjectName) loadData(currentSemesterId, name, currentIsSemester)
+ }
+ }
+
+ fun onTypeChange(isSemester: Boolean) {
+ Timber.i("Select attendance stats semester: $isSemester")
+ view?.run {
+ showContent(false)
+ showProgress(true)
+ showEmpty(false)
+ clearView()
+ }
+ loadData(currentSemesterId, currentSubjectName, isSemester)
+ }
+
+ private fun loadSubjects() {
+ Timber.i("Loading grade stats subjects started")
+ disposable.add(studentRepository.getCurrentStudent()
+ .flatMap { semesterRepository.getCurrentSemester(it) }
+ .flatMap { subjectRepository.getSubjects(it) }
+ .doOnSuccess { subjects = it }
+ .map { ArrayList(it.map { subject -> subject.name }) }
+ .subscribeOn(schedulers.backgroundThread)
+ .observeOn(schedulers.mainThread)
+ .subscribe({
+ Timber.i("Loading grade stats subjects result: Success")
+ view?.run {
+ updateSubjects(it)
+ showSubjects(true)
+ }
+ }, {
+ Timber.e("Loading grade stats subjects result: An exception occurred")
+ errorHandler.dispatch(it)
+ })
+ )
+ }
+
+ private fun loadData(semesterId: Int, subjectName: String, isSemester: Boolean, forceRefresh: Boolean = false) {
+ Timber.i("Loading grade stats data started")
+ currentSubjectName = subjectName
+ currentIsSemester = isSemester
+ disposable.add(studentRepository.getCurrentStudent()
+ .flatMap { semesterRepository.getSemesters(it) }
+ .flatMap { gradeStatisticsRepository.getGradesStatistics(it.first { item -> item.semesterId == semesterId }, subjectName, isSemester, forceRefresh) }
+ .map { list -> list.sortedByDescending { it.grade } }
+ .map { list -> list.filter { it.amount != 0 } }
+ .subscribeOn(schedulers.backgroundThread)
+ .observeOn(schedulers.mainThread)
+ .doFinally {
+ view?.run {
+ showRefresh(false)
+ showProgress(false)
+ notifyParentDataLoaded(semesterId)
+ }
+ }
+ .subscribe({
+ Timber.i("Loading grade stats result: Success")
+ view?.run {
+ showEmpty(it.isEmpty())
+ showContent(it.isNotEmpty())
+ updateData(it)
+ }
+ analytics.logEvent("load_grade_statistics", "items" to it.size, "force_refresh" to forceRefresh)
+ }) {
+ Timber.e("Loading grade stats result: An exception occurred")
+ view?.run { showEmpty(isViewEmpty) }
+ errorHandler.dispatch(it)
+ })
+ }
+}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsView.kt
new file mode 100644
index 000000000..59a71c003
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsView.kt
@@ -0,0 +1,31 @@
+package io.github.wulkanowy.ui.modules.grade.statistics
+
+import io.github.wulkanowy.data.db.entities.GradeStatistics
+import io.github.wulkanowy.ui.base.session.BaseSessionView
+
+interface GradeStatisticsView : BaseSessionView {
+
+ val isViewEmpty: Boolean
+
+ fun initView()
+
+ fun updateSubjects(data: ArrayList)
+
+ fun updateData(items: List)
+
+ fun showSubjects(show: Boolean)
+
+ fun notifyParentDataLoaded(semesterId: Int)
+
+ fun notifyParentRefresh()
+
+ fun clearView()
+
+ fun showContent(show: Boolean)
+
+ fun showEmpty(show: Boolean)
+
+ fun showProgress(show: Boolean)
+
+ fun showRefresh(show: Boolean)
+}
diff --git a/app/src/main/java/io/github/wulkanowy/utils/SpinnerExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/SpinnerExtension.kt
index bea2bd3dc..d9e34e87c 100644
--- a/app/src/main/java/io/github/wulkanowy/utils/SpinnerExtension.kt
+++ b/app/src/main/java/io/github/wulkanowy/utils/SpinnerExtension.kt
@@ -4,11 +4,19 @@ import android.view.View
import android.widget.AdapterView
import android.widget.Spinner
+/**
+ * @see How to keep onItemSelected from firing off on a newly instantiated Spinner?
+ */
inline fun Spinner.setOnItemSelectedListener(crossinline listener: (view: View?) -> Unit) {
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
- listener(view)
+ onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+ override fun onNothingSelected(parent: AdapterView<*>?) {}
+ override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+ listener(view)
+ }
+ }
}
}
}
diff --git a/app/src/main/res/layout/fragment_grade.xml b/app/src/main/res/layout/fragment_grade.xml
index 522312a28..6cd226361 100644
--- a/app/src/main/res/layout/fragment_grade.xml
+++ b/app/src/main/res/layout/fragment_grade.xml
@@ -5,32 +5,33 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
-
+ app:tabMode="scrollable"
+ app:tabTextColor="@android:color/white"
+ tools:visibility="visible" />
+ android:visibility="invisible"
+ tools:visibility="visible" />
+ android:indeterminate="true"
+ tools:visibility="invisible" />
+ android:visibility="invisible"
+ tools:visibility="visible">
+ android:layout_height="match_parent"
+ tools:context=".ui.modules.grade.details.GradeDetailsFragment">
+ android:indeterminate="true"
+ tools:visibility="gone" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_grade_summary.xml b/app/src/main/res/layout/fragment_grade_summary.xml
index 37b970287..92a34f71c 100644
--- a/app/src/main/res/layout/fragment_grade_summary.xml
+++ b/app/src/main/res/layout/fragment_grade_summary.xml
@@ -2,7 +2,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ tools:context=".ui.modules.grade.summary.GradeSummaryFragment">
+ android:indeterminate="true"
+ tools:visibility="gone" />
Obliczona średnia
Końcowa średnia
Podsumowanie
+ Oceny klasy
Oznacz jako przeczytane
+ Oceny cząstkowe
+ Oceny semestralne
- %d ocena
- %d oceny
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d6c3612e9..4a2cc9167 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -60,7 +60,10 @@
Calculated average
Final average
Summary
+ Class grades
Mark as read
+ Partial grades
+ Semester grades
- %d grade
- %d grades
diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/remote/GradeStatisticsRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/remote/GradeStatisticsRemoteTest.kt
new file mode 100644
index 000000000..14381791d
--- /dev/null
+++ b/app/src/test/java/io/github/wulkanowy/data/repositories/remote/GradeStatisticsRemoteTest.kt
@@ -0,0 +1,53 @@
+package io.github.wulkanowy.data.repositories.remote
+
+import io.github.wulkanowy.api.Api
+import io.github.wulkanowy.api.grades.GradeStatistics
+import io.github.wulkanowy.data.db.entities.Semester
+import io.github.wulkanowy.data.repositories.gradestatistics.GradeStatisticsRemote
+import io.mockk.MockKAnnotations
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.impl.annotations.SpyK
+import io.reactivex.Single
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+
+class GradeStatisticsRemoteTest {
+
+ @SpyK
+ private var mockApi = Api()
+
+ @MockK
+ private lateinit var semesterMock: Semester
+
+ @Before
+ fun initApi() {
+ MockKAnnotations.init(this)
+ }
+
+ @Test
+ fun getGradeStatisticsTest() {
+ every { mockApi.getGradesStatistics(1, any()) } returns Single.just(listOf(
+ getGradeStatistics("Fizyka"),
+ getGradeStatistics("Matematyka")
+ ))
+
+ every { mockApi.diaryId } returns 1
+ every { semesterMock.studentId } returns 1
+ every { semesterMock.semesterId } returns 1
+ every { semesterMock.semesterName } returns 2
+ every { semesterMock.diaryId } returns 1
+
+ val stats = GradeStatisticsRemote(mockApi).getGradeStatistics(semesterMock, false).blockingGet()
+ Assert.assertEquals(2, stats.size)
+ }
+
+ private fun getGradeStatistics(subjectName: String): GradeStatistics {
+ return GradeStatistics().apply {
+ subject = subjectName
+ gradeValue = 5
+ amount = 10
+ }
+ }
+}