forked from github/wulkanowy-mirror
Add grade statistics (#251)
This commit is contained in:
parent
cae4f140e6
commit
dcab8df4b9
@ -97,6 +97,9 @@ dependencies {
|
|||||||
implementation "com.aurelhubert:ahbottomnavigation:2.3.4"
|
implementation "com.aurelhubert:ahbottomnavigation:2.3.4"
|
||||||
implementation 'com.ncapdevi:frag-nav:3.1.0'
|
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 'com.github.pwittchen:reactivenetwork-rx2:3.0.2'
|
||||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
||||||
implementation "io.reactivex.rxjava2:rxjava:2.2.5"
|
implementation "io.reactivex.rxjava2:rxjava:2.2.5"
|
||||||
@ -125,8 +128,6 @@ dependencies {
|
|||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
|
||||||
androidTestImplementation 'org.mockito:mockito-android:2.23.4'
|
androidTestImplementation 'org.mockito:mockito-android:2.23.4'
|
||||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||||
|
|
||||||
implementation "com.hootsuite.android:nachos:1.1.1"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
@ -32,5 +32,8 @@
|
|||||||
-dontwarn rx.internal.util.**
|
-dontwarn rx.internal.util.**
|
||||||
-dontwarn sun.misc.Unsafe
|
-dontwarn sun.misc.Unsafe
|
||||||
|
|
||||||
|
#Config for MPAndroidChart
|
||||||
|
-keep class com.github.mikephil.charting.** { *; }
|
||||||
|
|
||||||
#Config for API
|
#Config for API
|
||||||
-keep class io.github.wulkanowy.api.** {*;}
|
-keep class io.github.wulkanowy.api.** {*;}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@
|
|||||||
package="io.github.wulkanowy"
|
package="io.github.wulkanowy"
|
||||||
android:installLocation="internalOnly">
|
android:installLocation="internalOnly">
|
||||||
|
|
||||||
<uses-sdk tools:overrideLibrary="com.readystatesoftware.chuck"/>
|
<uses-sdk tools:overrideLibrary="com.readystatesoftware.chuck" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
@ -20,7 +20,7 @@
|
|||||||
android:supportsRtl="false"
|
android:supportsRtl="false"
|
||||||
android:theme="@style/WulkanowyTheme"
|
android:theme="@style/WulkanowyTheme"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.modules.splash.SplashActivity"
|
android:name=".ui.modules.splash.SplashActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
|
@ -82,6 +82,10 @@ internal class RepositoryModule {
|
|||||||
@Provides
|
@Provides
|
||||||
fun provideGradeSummaryDao(database: AppDatabase) = database.gradeSummaryDao
|
fun provideGradeSummaryDao(database: AppDatabase) = database.gradeSummaryDao
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideGradeStatisticsDao(database: AppDatabase) = database.gradeStatistics
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideMessagesDao(database: AppDatabase) = database.messagesDao
|
fun provideMessagesDao(database: AppDatabase) = database.messagesDao
|
||||||
|
@ -11,6 +11,7 @@ import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
|
|||||||
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
|
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
|
||||||
import io.github.wulkanowy.data.db.dao.ExamDao
|
import io.github.wulkanowy.data.db.dao.ExamDao
|
||||||
import io.github.wulkanowy.data.db.dao.GradeDao
|
import io.github.wulkanowy.data.db.dao.GradeDao
|
||||||
|
import io.github.wulkanowy.data.db.dao.GradeStatisticsDao
|
||||||
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
|
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
|
||||||
import io.github.wulkanowy.data.db.dao.HomeworkDao
|
import io.github.wulkanowy.data.db.dao.HomeworkDao
|
||||||
import io.github.wulkanowy.data.db.dao.LuckyNumberDao
|
import io.github.wulkanowy.data.db.dao.LuckyNumberDao
|
||||||
@ -27,6 +28,7 @@ import io.github.wulkanowy.data.db.entities.AttendanceSummary
|
|||||||
import io.github.wulkanowy.data.db.entities.CompletedLesson
|
import io.github.wulkanowy.data.db.entities.CompletedLesson
|
||||||
import io.github.wulkanowy.data.db.entities.Exam
|
import io.github.wulkanowy.data.db.entities.Exam
|
||||||
import io.github.wulkanowy.data.db.entities.Grade
|
import io.github.wulkanowy.data.db.entities.Grade
|
||||||
|
import io.github.wulkanowy.data.db.entities.GradeStatistics
|
||||||
import io.github.wulkanowy.data.db.entities.GradeSummary
|
import io.github.wulkanowy.data.db.entities.GradeSummary
|
||||||
import io.github.wulkanowy.data.db.entities.Homework
|
import io.github.wulkanowy.data.db.entities.Homework
|
||||||
import io.github.wulkanowy.data.db.entities.LuckyNumber
|
import io.github.wulkanowy.data.db.entities.LuckyNumber
|
||||||
@ -43,6 +45,7 @@ import io.github.wulkanowy.data.db.migrations.Migration3
|
|||||||
import io.github.wulkanowy.data.db.migrations.Migration4
|
import io.github.wulkanowy.data.db.migrations.Migration4
|
||||||
import io.github.wulkanowy.data.db.migrations.Migration5
|
import io.github.wulkanowy.data.db.migrations.Migration5
|
||||||
import io.github.wulkanowy.data.db.migrations.Migration6
|
import io.github.wulkanowy.data.db.migrations.Migration6
|
||||||
|
import io.github.wulkanowy.data.db.migrations.Migration7
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@ -56,6 +59,7 @@ import javax.inject.Singleton
|
|||||||
AttendanceSummary::class,
|
AttendanceSummary::class,
|
||||||
Grade::class,
|
Grade::class,
|
||||||
GradeSummary::class,
|
GradeSummary::class,
|
||||||
|
GradeStatistics::class,
|
||||||
Message::class,
|
Message::class,
|
||||||
Note::class,
|
Note::class,
|
||||||
Homework::class,
|
Homework::class,
|
||||||
@ -72,7 +76,7 @@ import javax.inject.Singleton
|
|||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val VERSION_SCHEMA = 6
|
const val VERSION_SCHEMA = 7
|
||||||
|
|
||||||
fun newInstance(context: Context): AppDatabase {
|
fun newInstance(context: Context): AppDatabase {
|
||||||
return Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database")
|
return Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database")
|
||||||
@ -84,7 +88,8 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
Migration3(),
|
Migration3(),
|
||||||
Migration4(),
|
Migration4(),
|
||||||
Migration5(),
|
Migration5(),
|
||||||
Migration6()
|
Migration6(),
|
||||||
|
Migration7()
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
@ -106,6 +111,8 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
|
|
||||||
abstract val gradeSummaryDao: GradeSummaryDao
|
abstract val gradeSummaryDao: GradeSummaryDao
|
||||||
|
|
||||||
|
abstract val gradeStatistics: GradeStatisticsDao
|
||||||
|
|
||||||
abstract val messagesDao: MessagesDao
|
abstract val messagesDao: MessagesDao
|
||||||
|
|
||||||
abstract val noteDao: NoteDao
|
abstract val noteDao: NoteDao
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
package io.github.wulkanowy.data.db.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Query
|
||||||
|
import io.github.wulkanowy.data.db.entities.GradeStatistics
|
||||||
|
import io.reactivex.Maybe
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Dao
|
||||||
|
interface GradeStatisticsDao {
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun insertAll(gradesStatistics: List<GradeStatistics>)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun deleteAll(gradesStatistics: List<GradeStatistics>)
|
||||||
|
|
||||||
|
@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<List<GradeStatistics>>
|
||||||
|
|
||||||
|
@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<List<GradeStatistics>>
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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)")
|
||||||
|
}
|
||||||
|
}
|
@ -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<List<GradeStatistics>> {
|
||||||
|
return gradeStatisticsDb.loadAll(semester.semesterId, semester.studentId, isSemester)
|
||||||
|
.filter { !it.isEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getGradesStatistics(semester: Semester, isSemester: Boolean, subjectName: String): Maybe<List<GradeStatistics>> {
|
||||||
|
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<GradeStatistics>) {
|
||||||
|
gradeStatisticsDb.insertAll(gradesStatistics)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteGradesStatistics(gradesStatistics: List<GradeStatistics>) {
|
||||||
|
gradeStatisticsDb.deleteAll(gradesStatistics)
|
||||||
|
}
|
||||||
|
}
|
@ -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<List<GradeStatistics>> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<List<GradeStatistics>> {
|
||||||
|
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()) })
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,7 @@ import javax.inject.Inject
|
|||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class SubjectRepostory @Inject constructor(
|
class SubjectRepository @Inject constructor(
|
||||||
private val settings: InternetObservingSettings,
|
private val settings: InternetObservingSettings,
|
||||||
private val local: SubjectLocal,
|
private val local: SubjectLocal,
|
||||||
private val remote: SubjectRemote
|
private val remote: SubjectRemote
|
@ -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.attendancesummary.AttendanceSummaryRepository
|
||||||
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
|
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
|
||||||
import io.github.wulkanowy.data.repositories.student.StudentRepository
|
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.BaseSessionPresenter
|
||||||
import io.github.wulkanowy.ui.base.session.SessionErrorHandler
|
import io.github.wulkanowy.ui.base.session.SessionErrorHandler
|
||||||
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
|
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
|
||||||
@ -21,7 +21,7 @@ import javax.inject.Inject
|
|||||||
class AttendanceSummaryPresenter @Inject constructor(
|
class AttendanceSummaryPresenter @Inject constructor(
|
||||||
private val errorHandler: SessionErrorHandler,
|
private val errorHandler: SessionErrorHandler,
|
||||||
private val attendanceSummaryRepository: AttendanceSummaryRepository,
|
private val attendanceSummaryRepository: AttendanceSummaryRepository,
|
||||||
private val subjectRepository: SubjectRepostory,
|
private val subjectRepository: SubjectRepository,
|
||||||
private val studentRepository: StudentRepository,
|
private val studentRepository: StudentRepository,
|
||||||
private val semesterRepository: SemesterRepository,
|
private val semesterRepository: SemesterRepository,
|
||||||
private val schedulers: SchedulersProvider,
|
private val schedulers: SchedulersProvider,
|
||||||
|
@ -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 <a href="https://stackoverflow.com/a/50382854">Tabs don't fit to screen with tabmode=scrollable, Even with a Custom Tab Layout</a>
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ import io.github.wulkanowy.R
|
|||||||
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
|
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
|
||||||
import io.github.wulkanowy.ui.base.session.BaseSessionFragment
|
import io.github.wulkanowy.ui.base.session.BaseSessionFragment
|
||||||
import io.github.wulkanowy.ui.modules.grade.details.GradeDetailsFragment
|
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.grade.summary.GradeSummaryFragment
|
||||||
import io.github.wulkanowy.ui.modules.main.MainView
|
import io.github.wulkanowy.ui.modules.main.MainView
|
||||||
import io.github.wulkanowy.utils.setOnSelectPageListener
|
import io.github.wulkanowy.utils.setOnSelectPageListener
|
||||||
@ -63,12 +64,14 @@ class GradeFragment : BaseSessionFragment(), GradeView, MainView.MainChildView,
|
|||||||
containerId = gradeViewPager.id
|
containerId = gradeViewPager.id
|
||||||
addFragmentsWithTitle(mapOf(
|
addFragmentsWithTitle(mapOf(
|
||||||
GradeDetailsFragment.newInstance() to getString(R.string.all_details),
|
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 {
|
gradeViewPager.run {
|
||||||
adapter = pagerAdapter
|
adapter = pagerAdapter
|
||||||
|
offscreenPageLimit = 3
|
||||||
setOnSelectPageListener { presenter.onPageSelected(it) }
|
setOnSelectPageListener { presenter.onPageSelected(it) }
|
||||||
}
|
}
|
||||||
gradeTabLayout.setupWithViewPager(gradeViewPager)
|
gradeTabLayout.setupWithViewPager(gradeViewPager)
|
||||||
|
@ -7,6 +7,7 @@ import io.github.wulkanowy.di.scopes.PerChildFragment
|
|||||||
import io.github.wulkanowy.di.scopes.PerFragment
|
import io.github.wulkanowy.di.scopes.PerFragment
|
||||||
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
|
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
|
||||||
import io.github.wulkanowy.ui.modules.grade.details.GradeDetailsFragment
|
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.grade.summary.GradeSummaryFragment
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ -28,4 +29,8 @@ abstract class GradeModule {
|
|||||||
@PerChildFragment
|
@PerChildFragment
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun binGradeSummaryFragment(): GradeSummaryFragment
|
abstract fun binGradeSummaryFragment(): GradeSummaryFragment
|
||||||
|
|
||||||
|
@PerChildFragment
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun binGradeStatisticsFragment(): GradeStatisticsFragment
|
||||||
}
|
}
|
||||||
|
@ -114,6 +114,6 @@ class GradePresenter @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyChildrenSemesterChange() {
|
private fun notifyChildrenSemesterChange() {
|
||||||
for (i in 0..1) view?.notifyChildSemesterChange(i)
|
for (i in 0..2) view?.notifyChildSemesterChange(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<String>
|
||||||
|
|
||||||
|
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<String>())
|
||||||
|
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<String>) {
|
||||||
|
subjectsAdapter.run {
|
||||||
|
clear()
|
||||||
|
addAll(data)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateData(items: List<GradeStatistics>) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
@ -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<GradeStatisticsView>(errorHandler) {
|
||||||
|
|
||||||
|
private var subjects = emptyList<Subject>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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<String>)
|
||||||
|
|
||||||
|
fun updateData(items: List<GradeStatistics>)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
@ -4,11 +4,19 @@ import android.view.View
|
|||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see <a href="https://stackoverflow.com/a/29602298">How to keep onItemSelected from firing off on a newly instantiated Spinner?</a>
|
||||||
|
*/
|
||||||
inline fun Spinner.setOnItemSelectedListener(crossinline listener: (view: View?) -> Unit) {
|
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) {
|
||||||
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
listener(view)
|
listener(view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,32 +5,33 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
<io.github.wulkanowy.ui.modules.grade.CustomTabLayout
|
||||||
android:id="@+id/gradeTabLayout"
|
android:id="@+id/gradeTabLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:background="@color/colorPrimary"
|
android:background="@color/colorPrimary"
|
||||||
android:elevation="5dp"
|
android:elevation="5dp"
|
||||||
android:visibility="invisible"
|
android:visibility="invisible"
|
||||||
app:tabGravity="fill"
|
|
||||||
app:tabIndicatorColor="@android:color/white"
|
app:tabIndicatorColor="@android:color/white"
|
||||||
app:tabMaxWidth="0dp"
|
app:tabMode="scrollable"
|
||||||
app:tabMode="fixed"
|
app:tabTextColor="@android:color/white"
|
||||||
app:tabTextColor="@android:color/white" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<androidx.viewpager.widget.ViewPager
|
<androidx.viewpager.widget.ViewPager
|
||||||
android:id="@+id/gradeViewPager"
|
android:id="@+id/gradeViewPager"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginTop="48dp"
|
android:layout_marginTop="48dp"
|
||||||
android:visibility="invisible" />
|
android:visibility="invisible"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/gradeProgress"
|
android:id="@+id/gradeProgress"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:indeterminate="true" />
|
android:indeterminate="true"
|
||||||
|
tools:visibility="invisible" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/gradeEmpty"
|
android:id="@+id/gradeEmpty"
|
||||||
@ -38,7 +39,8 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:visibility="invisible">
|
android:visibility="invisible"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageView
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".ui.modules.grade.details.GradeDetailsFragment">
|
||||||
|
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/gradeDetailsSwipe"
|
android:id="@+id/gradeDetailsSwipe"
|
||||||
@ -20,7 +21,8 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:indeterminate="true" />
|
android:indeterminate="true"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/gradeDetailsEmpty"
|
android:id="@+id/gradeDetailsEmpty"
|
||||||
|
128
app/src/main/res/layout/fragment_grade_statistics.xml
Normal file
128
app/src/main/res/layout/fragment_grade_statistics.xml
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
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:orientation="vertical"
|
||||||
|
tools:context=".ui.modules.grade.statistics.GradeStatisticsFragment">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/gradeStatisticsSubjectsContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:attr/windowBackground"
|
||||||
|
android:elevation="5dp"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:visibility="invisible"
|
||||||
|
tools:listitem="@layout/item_attendance_summary"
|
||||||
|
tools:targetApi="lollipop"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatSpinner
|
||||||
|
android:id="@+id/gradeStatisticsSubjects"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="middle"
|
||||||
|
android:entries="@array/endpoints_keys"
|
||||||
|
android:paddingStart="10dp"
|
||||||
|
android:paddingLeft="10dp"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingEnd="30dp"
|
||||||
|
android:paddingRight="30dp"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
android:spinnerMode="dialog" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/gradeStatisticsSwipe"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/gradeStatisticsTypeSwitch"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingTop="5dp">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/gradeStatisticsTypePartial"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="true"
|
||||||
|
android:tag="partial"
|
||||||
|
android:text="@string/grade_statistics_partial" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/gradeStatisticsTypeSemester"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:tag="annual"
|
||||||
|
android:text="@string/grade_statistics_semester" />
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.github.mikephil.charting.charts.PieChart
|
||||||
|
android:id="@+id/gradeStatisticsChart"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?android:attr/windowBackground"
|
||||||
|
android:minHeight="400dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_margin="10dp"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/gradeStatisticsProgress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:indeterminate="true"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/gradeStatisticsEmpty"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="invisible">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="100dp"
|
||||||
|
android:minHeight="100dp"
|
||||||
|
app:srcCompat="@drawable/ic_menu_main_grade_26dp"
|
||||||
|
app:tint="?android:attr/textColorPrimary"
|
||||||
|
tools:ignore="contentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/grade_no_items"
|
||||||
|
android:textSize="20sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
</LinearLayout>
|
@ -2,7 +2,8 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".ui.modules.grade.summary.GradeSummaryFragment">
|
||||||
|
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/gradeSummarySwipe"
|
android:id="@+id/gradeSummarySwipe"
|
||||||
@ -20,7 +21,8 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:indeterminate="true" />
|
android:indeterminate="true"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/gradeSummaryEmpty"
|
android:id="@+id/gradeSummaryEmpty"
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:layout_marginRight="16dp"
|
android:layout_marginRight="16dp"
|
||||||
android:tint="?android:attr/textColorPrimary"
|
android:tint="@android:color/black"
|
||||||
app:srcCompat="@drawable/ic_all_account_24dp" />
|
app:srcCompat="@drawable/ic_all_account_24dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -60,7 +60,10 @@
|
|||||||
<string name="grade_summary_calculated_average">Obliczona średnia</string>
|
<string name="grade_summary_calculated_average">Obliczona średnia</string>
|
||||||
<string name="grade_summary_final_average">Końcowa średnia</string>
|
<string name="grade_summary_final_average">Końcowa średnia</string>
|
||||||
<string name="grade_menu_summary">Podsumowanie</string>
|
<string name="grade_menu_summary">Podsumowanie</string>
|
||||||
|
<string name="grade_menu_statistics">Oceny klasy</string>
|
||||||
<string name="grade_menu_read">Oznacz jako przeczytane</string>
|
<string name="grade_menu_read">Oznacz jako przeczytane</string>
|
||||||
|
<string name="grade_statistics_partial">Oceny cząstkowe</string>
|
||||||
|
<string name="grade_statistics_semester">Oceny semestralne</string>
|
||||||
<plurals name="grade_number_item">
|
<plurals name="grade_number_item">
|
||||||
<item quantity="one">%d ocena</item>
|
<item quantity="one">%d ocena</item>
|
||||||
<item quantity="few">%d oceny</item>
|
<item quantity="few">%d oceny</item>
|
||||||
|
@ -60,7 +60,10 @@
|
|||||||
<string name="grade_summary_calculated_average">Calculated average</string>
|
<string name="grade_summary_calculated_average">Calculated average</string>
|
||||||
<string name="grade_summary_final_average">Final average</string>
|
<string name="grade_summary_final_average">Final average</string>
|
||||||
<string name="grade_menu_summary">Summary</string>
|
<string name="grade_menu_summary">Summary</string>
|
||||||
|
<string name="grade_menu_statistics">Class grades</string>
|
||||||
<string name="grade_menu_read">Mark as read</string>
|
<string name="grade_menu_read">Mark as read</string>
|
||||||
|
<string name="grade_statistics_partial">Partial grades</string>
|
||||||
|
<string name="grade_statistics_semester">Semester grades</string>
|
||||||
<plurals name="grade_number_item">
|
<plurals name="grade_number_item">
|
||||||
<item quantity="one">%d grade</item>
|
<item quantity="one">%d grade</item>
|
||||||
<item quantity="other">%d grades</item>
|
<item quantity="other">%d grades</item>
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user