Add a summary of attendance (#132)

This commit is contained in:
Rafał Borcz 2018-12-07 19:01:19 +01:00 committed by Mikołaj Pich
parent 900065d758
commit f96d0ebed9
65 changed files with 1351 additions and 126 deletions

View File

@ -81,7 +81,7 @@ configurations.all {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation('com.github.wulkanowy:api:ba17abc') { exclude module: "threetenbp" }
implementation('com.github.wulkanowy:api:6a73b0e') { exclude module: "threetenbp" }
implementation "androidx.legacy:legacy-support-v4:1.0.0"
implementation "androidx.appcompat:appcompat:1.0.2"

View File

@ -1,3 +1,3 @@
-keep class android.support.test.internal** { *; }
-keep class org.junit.** { *; }
-keep public class io.github.wulkanowy** { *; }
-keep public class io.github.wulkanowy** { *; }

View File

@ -77,6 +77,10 @@ internal class RepositoryModule {
@Provides
fun provideAttendanceDao(database: AppDatabase) = database.attendanceDao
@Singleton
@Provides
fun provideAttendanceSummaryDao(database: AppDatabase) = database.attendanceSummaryDao
@Singleton
@Provides
fun provideTimetableDao(database: AppDatabase) = database.timetableDao
@ -88,4 +92,8 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideHomeworkDao(database: AppDatabase) = database.homeworkDao
@Singleton
@Provides
fun provideSubjectDao(database: AppDatabase) = database.subjectDao
}

View File

@ -7,6 +7,7 @@ import androidx.room.RoomDatabase
import androidx.room.RoomDatabase.JournalMode.TRUNCATE
import androidx.room.TypeConverters
import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
import io.github.wulkanowy.data.db.dao.ExamDao
import io.github.wulkanowy.data.db.dao.GradeDao
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
@ -15,8 +16,10 @@ import io.github.wulkanowy.data.db.dao.HomeworkDao
import io.github.wulkanowy.data.db.dao.NoteDao
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.dao.SubjectDao
import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
@ -25,6 +28,7 @@ import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Subject
import io.github.wulkanowy.data.db.entities.Timetable
import javax.inject.Singleton
@ -36,11 +40,13 @@ import javax.inject.Singleton
Exam::class,
Timetable::class,
Attendance::class,
AttendanceSummary::class,
Grade::class,
GradeSummary::class,
Message::class,
Note::class,
Homework::class
Homework::class,
Subject::class
],
version = 1,
exportSchema = false
@ -66,6 +72,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract val attendanceDao: AttendanceDao
abstract val attendanceSummaryDao: AttendanceSummaryDao
abstract val gradeDao: GradeDao
abstract val gradeSummaryDao: GradeSummaryDao
@ -75,4 +83,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val noteDao: NoteDao
abstract val homeworkDao: HomeworkDao
abstract val subjectDao: SubjectDao
}

View File

@ -1,8 +1,13 @@
package io.github.wulkanowy.data.db
import androidx.room.TypeConverter
import org.threeten.bp.*
import java.util.*
import org.threeten.bp.DateTimeUtils
import org.threeten.bp.Instant
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import org.threeten.bp.Month
import org.threeten.bp.ZoneOffset
import java.util.Date
class Converters {
@ -25,4 +30,10 @@ class Converters {
fun timeToTimestamp(date: LocalDateTime?): Long? {
return date?.atZone(ZoneOffset.UTC)?.toInstant()?.toEpochMilli()
}
@TypeConverter
fun monthToInt(month: Month?) = month?.value
@TypeConverter
fun intToMonth(value: Int?) = value?.let { Month.of(it) }
}

View File

@ -20,5 +20,5 @@ interface AttendanceDao {
fun deleteAll(exams: List<Attendance>)
@Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end")
fun load(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Maybe<List<Attendance>>
fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Maybe<List<Attendance>>
}

View File

@ -0,0 +1,21 @@
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.AttendanceSummary
import io.reactivex.Maybe
@Dao
interface AttendanceSummaryDao {
@Insert
fun insertAll(exams: List<AttendanceSummary>): List<Long>
@Delete
fun deleteAll(exams: List<AttendanceSummary>)
@Query("SELECT * FROM AttendanceSummary WHERE diary_id = :diaryId AND student_id = :studentId AND subject_id = :subjectId")
fun loadAll(diaryId: Int, studentId: Int, subjectId: Int): Maybe<List<AttendanceSummary>>
}

View File

@ -20,5 +20,5 @@ interface ExamDao {
fun deleteAll(exams: List<Exam>)
@Query("SELECT * FROM Exams WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end")
fun load(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Maybe<List<Exam>>
fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Maybe<List<Exam>>
}

View File

@ -26,8 +26,8 @@ interface GradeDao {
fun deleteAll(grades: List<Grade>)
@Query("SELECT * FROM Grades WHERE semester_id = :semesterId AND student_id = :studentId")
fun load(semesterId: Int, studentId: Int): Maybe<List<Grade>>
fun loadAll(semesterId: Int, studentId: Int): Maybe<List<Grade>>
@Query("SELECT * FROM Grades WHERE is_read = 0 AND semester_id = :semesterId AND student_id = :studentId")
fun loadNew(semesterId: Int, studentId: Int): Maybe<List<Grade>>
fun loadAllNew(semesterId: Int, studentId: Int): Maybe<List<Grade>>
}

View File

@ -19,5 +19,5 @@ interface GradeSummaryDao {
fun deleteAll(gradesSummary: List<GradeSummary>)
@Query("SELECT * FROM grades_summary WHERE student_id = :studentId AND semester_id = :semesterId")
fun load(semesterId: Int, studentId: Int): Maybe<List<GradeSummary>>
fun loadAll(semesterId: Int, studentId: Int): Maybe<List<GradeSummary>>
}

View File

@ -20,5 +20,5 @@ interface HomeworkDao {
fun deleteAll(homework: List<Homework>)
@Query("SELECT * FROM Homework WHERE semester_id = :semesterId AND student_id = :studentId AND date = :date")
fun load(semesterId: Int, studentId: Int, date: LocalDate): Maybe<List<Homework>>
fun loadAll(semesterId: Int, studentId: Int, date: LocalDate): Maybe<List<Homework>>
}

View File

@ -26,7 +26,7 @@ interface NoteDao {
fun deleteAll(notes: List<Note>)
@Query("SELECT * FROM Notes WHERE semester_id = :semesterId AND student_id = :studentId")
fun load(semesterId: Int, studentId: Int): Maybe<List<Note>>
fun loadAll(semesterId: Int, studentId: Int): Maybe<List<Note>>
@Query("SELECT * FROM Notes WHERE is_read = 0 AND semester_id = :semesterId AND student_id = :studentId")
fun loadNew(semesterId: Int, studentId: Int): Maybe<List<Note>>

View File

@ -16,7 +16,7 @@ interface SemesterDao {
fun insertAll(semester: List<Semester>)
@Query("SELECT * FROM Semesters WHERE student_id = :studentId")
fun load(studentId: Int): Maybe<List<Semester>>
fun loadAll(studentId: Int): Maybe<List<Semester>>
@Query("UPDATE Semesters SET is_current = 1 WHERE semester_id = :semesterId AND diary_id = :diaryId")
fun updateCurrent(semesterId: Int, diaryId: Int)

View File

@ -0,0 +1,21 @@
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.Subject
import io.reactivex.Maybe
@Dao
interface SubjectDao {
@Insert
fun insertAll(subjects: List<Subject>): List<Long>
@Delete
fun deleteAll(subjects: List<Subject>)
@Query("SELECT * FROM Subjects WHERE diary_id = :diaryId AND student_id = :studentId")
fun loadAll(diaryId: Int, studentId: Int): Maybe<List<Subject>>
}

View File

@ -20,5 +20,5 @@ interface TimetableDao {
fun deleteAll(exams: List<Timetable>)
@Query("SELECT * FROM Timetable WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end")
fun load(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Maybe<List<Timetable>>
fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Maybe<List<Timetable>>
}

View File

@ -9,31 +9,31 @@ import java.io.Serializable
@Entity(tableName = "Attendance")
data class Attendance(
@ColumnInfo(name = "student_id")
var studentId: Int,
@ColumnInfo(name = "student_id")
var studentId: Int,
@ColumnInfo(name = "diary_id")
var diaryId: Int,
@ColumnInfo(name = "diary_id")
var diaryId: Int,
var date: LocalDate,
var date: LocalDate,
var number: Int,
var number: Int,
var subject: String,
var subject: String,
var name: String,
var name: String,
var presence: Boolean = false,
var presence: Boolean = false,
var absence: Boolean = false,
var absence: Boolean = false,
var exemption: Boolean = false,
var exemption: Boolean = false,
var lateness: Boolean = false,
var lateness: Boolean = false,
var excused: Boolean = false,
var excused: Boolean = false,
var deleted: Boolean = false
var deleted: Boolean = false
) : Serializable {
@PrimaryKey(autoGenerate = true)

View File

@ -0,0 +1,43 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.threeten.bp.Month
import java.io.Serializable
@Entity(tableName = "AttendanceSummary")
data class AttendanceSummary(
@ColumnInfo(name = "student_id")
var studentId: Int,
@ColumnInfo(name = "diary_id")
var diaryId: Int,
@ColumnInfo(name = "subject_id")
var subjectId: Int = 0,
val month: Month,
val presence: Int,
val absence: Int,
@ColumnInfo(name = "absence_excused")
val absenceExcused: Int,
@ColumnInfo(name = "absence_for_school_reasons")
val absenceForSchoolReasons: Int,
val lateness: Int,
@ColumnInfo(name = "lateness_excused")
val latenessExcused: Int,
val exemption: Int
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -0,0 +1,25 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@Entity(tableName = "Subjects")
data class Subject(
@ColumnInfo(name = "student_id")
var studentId: Int,
@ColumnInfo(name = "diary_id")
var diaryId: Int,
@ColumnInfo(name = "real_id")
var realId: Int,
var name: String
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -16,30 +16,30 @@ import javax.inject.Singleton
@Singleton
class AttendanceRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: AttendanceLocal,
private val remote: AttendanceRemote
private val settings: InternetObservingSettings,
private val local: AttendanceLocal,
private val remote: AttendanceRemote
) {
fun getAttendance(semester: Semester, startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean)
: Single<List<Attendance>> {
: Single<List<Attendance>> {
return Single.fromCallable { startDate.monday to endDate.friday }
.flatMap { dates ->
local.getAttendance(semester, dates.first, dates.second).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getAttendance(semester, dates.first, dates.second)
else Single.error(UnknownHostException())
}.flatMap { newAttendance ->
local.getAttendance(semester, dates.first, dates.second)
.toSingle(emptyList())
.doOnSuccess { oldAttendance ->
local.deleteAttendance(oldAttendance - newAttendance)
local.saveAttendance(newAttendance - oldAttendance)
}
}.flatMap {
local.getAttendance(semester, dates.first, dates.second)
.toSingle(emptyList())
}).map { list -> list.filter { it.date in startDate..endDate } }
}
.flatMap { dates ->
local.getAttendance(semester, dates.first, dates.second).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getAttendance(semester, dates.first, dates.second)
else Single.error(UnknownHostException())
}.flatMap { newAttendance ->
local.getAttendance(semester, dates.first, dates.second)
.toSingle(emptyList())
.doOnSuccess { oldAttendance ->
local.deleteAttendance(oldAttendance - newAttendance)
local.saveAttendance(newAttendance - oldAttendance)
}
}.flatMap {
local.getAttendance(semester, dates.first, dates.second)
.toSingle(emptyList())
}).map { list -> list.filter { it.date in startDate..endDate } }
}
}
}

View File

@ -0,0 +1,35 @@
package io.github.wulkanowy.data.repositories
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.AttendanceSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.repositories.local.AttendanceSummaryLocal
import io.github.wulkanowy.data.repositories.remote.AttendanceSummaryRemote
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AttendanceSummaryRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: AttendanceSummaryLocal,
private val remote: AttendanceSummaryRemote
) {
fun getAttendanceSummary(semester: Semester, subjectId: Int, forceRefresh: Boolean = false): Single<List<AttendanceSummary>>? {
return local.getAttendanceSummary(semester, subjectId).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getAttendanceSummary(semester, subjectId)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getAttendanceSummary(semester, subjectId).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteAttendanceSummary(old - new)
local.saveAttendanceSummary(new - old)
}
}.flatMap { local.getAttendanceSummary(semester, subjectId).toSingle(emptyList()) })
}
}

View File

@ -13,23 +13,23 @@ import javax.inject.Singleton
@Singleton
class GradeSummaryRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: GradeSummaryLocal,
private val remote: GradeSummaryRemote
private val settings: InternetObservingSettings,
private val local: GradeSummaryLocal,
private val remote: GradeSummaryRemote
) {
fun getGradesSummary(semester: Semester, forceRefresh: Boolean = false): Single<List<GradeSummary>> {
return local.getGradesSummary(semester).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getGradeSummary(semester)
else Single.error(UnknownHostException())
}.flatMap { newGradesSummary ->
local.getGradesSummary(semester).toSingle(emptyList())
.doOnSuccess { oldGradesSummary ->
local.deleteGradesSummary(oldGradesSummary - newGradesSummary)
local.saveGradesSummary(newGradesSummary - oldGradesSummary)
}
}.flatMap { local.getGradesSummary(semester).toSingle(emptyList()) })
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getGradeSummary(semester)
else Single.error(UnknownHostException())
}.flatMap { newGradesSummary ->
local.getGradesSummary(semester).toSingle(emptyList())
.doOnSuccess { oldGradesSummary ->
local.deleteGradesSummary(oldGradesSummary - newGradesSummary)
local.saveGradesSummary(newGradesSummary - oldGradesSummary)
}
}.flatMap { local.getGradesSummary(semester).toSingle(emptyList()) })
}
}

View File

@ -0,0 +1,38 @@
package io.github.wulkanowy.data.repositories
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.Semester
import io.github.wulkanowy.data.db.entities.Subject
import io.github.wulkanowy.data.repositories.local.SubjectLocal
import io.github.wulkanowy.data.repositories.remote.SubjectRemote
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SubjectRepostory @Inject constructor(
private val settings: InternetObservingSettings,
private val local: SubjectLocal,
private val remote: SubjectRemote
) {
fun getSubjects(semester: Semester, forceRefresh: Boolean = false): Single<List<Subject>> {
return local.getSubjects(semester).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getSubjects(semester)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getSubjects(semester)
.toSingle(emptyList())
.doOnSuccess { old ->
local.deleteSubjects(old - new)
local.saveSubjects(new - old)
}
}.flatMap {
local.getSubjects(semester).toSingle(emptyList())
})
}
}

View File

@ -16,31 +16,31 @@ import javax.inject.Singleton
@Singleton
class TimetableRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: TimetableLocal,
private val remote: TimetableRemote
private val settings: InternetObservingSettings,
private val local: TimetableLocal,
private val remote: TimetableRemote
) {
fun getTimetable(semester: Semester, startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false)
: Single<List<Timetable>> {
: Single<List<Timetable>> {
return Single.fromCallable { startDate.monday to endDate.friday }
.flatMap { dates ->
local.getTimetable(semester, dates.first, dates.second).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getTimetable(semester, dates.first, dates.second)
else Single.error(UnknownHostException())
}.flatMap { newTimetable ->
local.getTimetable(semester, dates.first, dates.second)
.toSingle(emptyList())
.doOnSuccess { oldTimetable ->
local.deleteTimetable(oldTimetable - newTimetable)
local.saveTimetable(newTimetable - oldTimetable)
}
}.flatMap {
local.getTimetable(semester, dates.first, dates.second)
.toSingle(emptyList())
}).map { list -> list.filter { it.date in startDate..endDate } }
}
.flatMap { dates ->
local.getTimetable(semester, dates.first, dates.second).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getTimetable(semester, dates.first, dates.second)
else Single.error(UnknownHostException())
}.flatMap { newTimetable ->
local.getTimetable(semester, dates.first, dates.second)
.toSingle(emptyList())
.doOnSuccess { oldTimetable ->
local.deleteTimetable(oldTimetable - newTimetable)
local.saveTimetable(newTimetable - oldTimetable)
}
}.flatMap {
local.getTimetable(semester, dates.first, dates.second)
.toSingle(emptyList())
}).map { list -> list.filter { it.date in startDate..endDate } }
}
}
}

View File

@ -10,7 +10,7 @@ import javax.inject.Inject
class AttendanceLocal @Inject constructor(private val attendanceDb: AttendanceDao) {
fun getAttendance(semester: Semester, startDate: LocalDate, endDate: LocalDate): Maybe<List<Attendance>> {
return attendanceDb.load(semester.diaryId, semester.studentId, startDate, endDate)
return attendanceDb.loadAll(semester.diaryId, semester.studentId, startDate, endDate)
.filter { !it.isEmpty() }
}

View File

@ -0,0 +1,22 @@
package io.github.wulkanowy.data.repositories.local
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.reactivex.Maybe
import javax.inject.Inject
class AttendanceSummaryLocal @Inject constructor(private val attendanceDb: AttendanceSummaryDao) {
fun getAttendanceSummary(semester: Semester, subjectId: Int): Maybe<List<AttendanceSummary>> {
return attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId).filter { !it.isEmpty() }
}
fun saveAttendanceSummary(attendance: List<AttendanceSummary>) {
attendanceDb.insertAll(attendance)
}
fun deleteAttendanceSummary(attendance: List<AttendanceSummary>) {
attendanceDb.deleteAll(attendance)
}
}

View File

@ -10,7 +10,7 @@ import javax.inject.Inject
class ExamLocal @Inject constructor(private val examDb: ExamDao) {
fun getExams(semester: Semester, startDate: LocalDate, endDate: LocalDate): Maybe<List<Exam>> {
return examDb.load(semester.diaryId, semester.studentId, startDate, endDate)
return examDb.loadAll(semester.diaryId, semester.studentId, startDate, endDate)
.filter { !it.isEmpty() }
}

View File

@ -12,11 +12,11 @@ import javax.inject.Singleton
class GradeLocal @Inject constructor(private val gradeDb: GradeDao) {
fun getGrades(semester: Semester): Maybe<List<Grade>> {
return gradeDb.load(semester.semesterId, semester.studentId).filter { !it.isEmpty() }
return gradeDb.loadAll(semester.semesterId, semester.studentId).filter { !it.isEmpty() }
}
fun getNewGrades(semester: Semester): Maybe<List<Grade>> {
return gradeDb.loadNew(semester.semesterId, semester.studentId)
return gradeDb.loadAllNew(semester.semesterId, semester.studentId)
}
fun saveGrades(grades: List<Grade>) {

View File

@ -11,7 +11,7 @@ import javax.inject.Singleton
class GradeSummaryLocal @Inject constructor(private val gradeSummaryDb: GradeSummaryDao) {
fun getGradesSummary(semester: Semester): Maybe<List<GradeSummary>> {
return gradeSummaryDb.load(semester.semesterId, semester.studentId)
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId)
.filter { !it.isEmpty() }
}

View File

@ -12,7 +12,7 @@ import javax.inject.Singleton
class HomeworkLocal @Inject constructor(private val homeworkDb: HomeworkDao) {
fun getHomework(semester: Semester, date: LocalDate): Maybe<List<Homework>> {
return homeworkDb.load(semester.semesterId, semester.studentId, date).filter { !it.isEmpty() }
return homeworkDb.loadAll(semester.semesterId, semester.studentId, date).filter { !it.isEmpty() }
}
fun saveHomework(homework: List<Homework>) {

View File

@ -12,7 +12,7 @@ import javax.inject.Singleton
class NoteLocal @Inject constructor(private val noteDb: NoteDao) {
fun getNotes(semester: Semester): Maybe<List<Note>> {
return noteDb.load(semester.semesterId, semester.studentId).filter { !it.isEmpty() }
return noteDb.loadAll(semester.semesterId, semester.studentId).filter { !it.isEmpty() }
}
fun getNewNotes(semester: Semester): Maybe<List<Note>> {

View File

@ -15,7 +15,7 @@ class SemesterLocal @Inject constructor(private val semesterDb: SemesterDao) {
}
fun getSemesters(student: Student): Maybe<List<Semester>> {
return semesterDb.load(student.studentId).filter { !it.isEmpty() }
return semesterDb.loadAll(student.studentId).filter { !it.isEmpty() }
}
fun setCurrentSemester(semester: Semester) {

View File

@ -0,0 +1,25 @@
package io.github.wulkanowy.data.repositories.local
import io.github.wulkanowy.data.db.dao.SubjectDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Subject
import io.reactivex.Maybe
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SubjectLocal @Inject constructor(private val subjectDao: SubjectDao) {
fun getSubjects(semester: Semester): Maybe<List<Subject>> {
return subjectDao.loadAll(semester.diaryId, semester.studentId)
.filter { !it.isEmpty() }
}
fun saveSubjects(subjects: List<Subject>) {
subjectDao.insertAll(subjects)
}
fun deleteSubjects(subjects: List<Subject>) {
subjectDao.deleteAll(subjects)
}
}

View File

@ -10,7 +10,7 @@ import javax.inject.Inject
class TimetableLocal @Inject constructor(private val timetableDb: TimetableDao) {
fun getTimetable(semester: Semester, startDate: LocalDate, endDate: LocalDate): Maybe<List<Timetable>> {
return timetableDb.load(semester.diaryId, semester.studentId, startDate, endDate)
return timetableDb.loadAll(semester.diaryId, semester.studentId, startDate, endDate)
.filter { !it.isEmpty() }
}

View File

@ -7,7 +7,9 @@ import io.github.wulkanowy.utils.toLocalDate
import io.reactivex.Single
import org.threeten.bp.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AttendanceRemote @Inject constructor(private val api: Api) {
fun getAttendance(semester: Semester, startDate: LocalDate, endDate: LocalDate): Single<List<Attendance>> {

View File

@ -0,0 +1,33 @@
package io.github.wulkanowy.data.repositories.remote
import io.github.wulkanowy.api.Api
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.reactivex.Single
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AttendanceSummaryRemote @Inject constructor(private val api: Api) {
fun getAttendanceSummary(semester: Semester, subjectId: Int): Single<List<AttendanceSummary>> {
return Single.just(api.apply { diaryId = semester.diaryId })
.flatMap { api.getAttendanceSummary(subjectId) }.map { attendance ->
attendance.map {
AttendanceSummary(
studentId = semester.studentId,
diaryId = semester.diaryId,
subjectId = subjectId,
month = it.month,
presence = it.presence,
absence = it.absence,
absenceExcused = it.absenceExcused,
absenceForSchoolReasons = it.absenceForSchoolReasons,
lateness = it.lateness,
latenessExcused = it.latenessExcused,
exemption = it.exemption
)
}
}
}
}

View File

@ -0,0 +1,27 @@
package io.github.wulkanowy.data.repositories.remote
import io.github.wulkanowy.api.Api
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Subject
import io.reactivex.Single
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SubjectRemote @Inject constructor(private val api: Api) {
fun getSubjects(semester: Semester): Single<List<Subject>> {
return Single.just(api.apply { diaryId = semester.diaryId })
.flatMap { api.getSubjects() }
.map { subjects ->
subjects.map {
Subject(
studentId = semester.studentId,
diaryId = semester.diaryId,
name = it.name,
realId = it.value
)
}
}
}
}

View File

@ -2,6 +2,9 @@ package io.github.wulkanowy.ui.modules.attendance
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter
@ -10,6 +13,8 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_attendance.*
@ -35,6 +40,14 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
override val isViewEmpty: Boolean
get() = attendanceAdapter.isEmpty
override val currentStackSize: Int?
get() = (activity as? MainActivity)?.currentStackSize
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_attendance, container, false)
}
@ -59,6 +72,15 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
attendanceNextButton.setOnClickListener { presenter.onNextDay() }
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
inflater?.inflate(R.menu.action_menu_attendance, menu)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
return if (item?.itemId == R.id.attendanceMenuSummary) presenter.onSummarySwitchSelected()
else false
}
override fun updateData(data: List<AttendanceItem>) {
attendanceAdapter.updateDataSet(data, true)
}
@ -72,13 +94,17 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
}
override fun resetView() {
attendanceAdapter.smoothScrollToPosition(0)
attendanceRecycler.smoothScrollToPosition(0)
}
override fun onFragmentReselected() {
presenter.onViewReselected()
}
override fun popView() {
(activity as? MainActivity)?.popView()
}
override fun showEmpty(show: Boolean) {
attendanceEmpty.visibility = if (show) View.VISIBLE else View.GONE
}
@ -107,6 +133,10 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
AttendanceDialog.newInstance(lesson).show(fragmentManager, lesson.toString())
}
override fun openSummaryView() {
(activity as? MainActivity)?.pushView(AttendanceSummaryFragment.newInstance())
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())

View File

@ -56,11 +56,15 @@ class AttendancePresenter @Inject constructor(
}
fun onViewReselected() {
now().previousOrSameSchoolDay.also {
if (currentDate != it) {
loadData(it)
reloadView()
} else view?.resetView()
view?.also { view ->
if (view.currentStackSize == 1) {
now().previousOrSameSchoolDay.also {
if (currentDate != it) {
loadData(it)
reloadView()
} else if (!view.isViewEmpty) view.resetView()
}
} else view.popView()
}
}
@ -68,6 +72,11 @@ class AttendancePresenter @Inject constructor(
if (item is AttendanceItem) view?.showAttendanceDialog(item.attendance)
}
fun onSummarySwitchSelected(): Boolean {
view?.openSummaryView()
return true
}
private fun loadData(date: LocalDate, forceRefresh: Boolean = false) {
currentDate = date
disposable.apply {

View File

@ -7,6 +7,8 @@ interface AttendanceView : BaseView {
val isViewEmpty: Boolean
val currentStackSize: Int?
fun initView()
fun updateData(data: List<AttendanceItem>)
@ -30,4 +32,8 @@ interface AttendanceView : BaseView {
fun showNextButton(show: Boolean)
fun showAttendanceDialog(lesson: Attendance)
fun openSummaryView()
fun popView()
}

View File

@ -0,0 +1,120 @@
package io.github.wulkanowy.ui.modules.attendance.summary
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.setOnItemSelectedListener
import kotlinx.android.synthetic.main.fragment_attendance_summary.*
import javax.inject.Inject
class AttendanceSummaryFragment : BaseFragment(), AttendanceSummaryView, MainView.TitledView {
@Inject
lateinit var presenter: AttendanceSummaryPresenter
@Inject
lateinit var attendanceSummaryAdapter: FlexibleAdapter<AbstractFlexibleItem<*>>
private lateinit var subjectsAdapter: ArrayAdapter<String>
companion object {
private const val SAVED_SUBJECT_KEY = "CURRENT_SUBJECT"
fun newInstance() = AttendanceSummaryFragment()
}
override val titleStringId: Int
get() = R.string.attendance_title
override val isViewEmpty
get() = attendanceSummaryAdapter.isEmpty
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_attendance_summary, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = attendanceSummaryRecycler
presenter.onAttachView(this, savedInstanceState?.getInt(SAVED_SUBJECT_KEY))
}
override fun initView() {
attendanceSummaryRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = attendanceSummaryAdapter
}
attendanceSummarySwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
context?.let {
subjectsAdapter = ArrayAdapter(it, android.R.layout.simple_spinner_item, ArrayList<String>())
subjectsAdapter.setDropDownViewResource(R.layout.item_attendance_summary_subject)
}
attendanceSummarySubjects.run {
adapter = subjectsAdapter
setOnItemSelectedListener { presenter.onSubjectSelected((it as TextView).text.toString()) }
}
}
override fun updateSubjects(data: ArrayList<String>) {
subjectsAdapter.run {
clear()
addAll(data)
notifyDataSetChanged()
}
}
override fun updateDataSet(data: List<AttendanceSummaryItem>, header: AttendanceSummaryScrollableHeader) {
attendanceSummaryAdapter.apply {
updateDataSet(data, true)
removeAllScrollableHeaders()
addScrollableHeader(header)
}
}
override fun clearView() {
attendanceSummaryAdapter.clear()
}
override fun showEmpty(show: Boolean) {
attendanceSummaryEmpty.visibility = if (show) VISIBLE else GONE
}
override fun showProgress(show: Boolean) {
attendanceSummaryProgress.visibility = if (show) VISIBLE else GONE
}
override fun showContent(show: Boolean) {
attendanceSummaryRecycler.visibility = if (show) VISIBLE else GONE
}
override fun showSubjects(show: Boolean) {
attendanceSummarySubjects.visibility = if (show) VISIBLE else VISIBLE
}
override fun hideRefresh() {
attendanceSummarySwipe.isRefreshing = false
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(SAVED_SUBJECT_KEY, presenter.currentSubjectId)
}
override fun onDestroyView() {
super.onDestroyView()
presenter.onDetachView()
}
}

View File

@ -0,0 +1,82 @@
package io.github.wulkanowy.ui.modules.attendance.summary
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_attendance_summary.*
class AttendanceSummaryItem(
private val month: String,
private val percentage: String,
private val present: String,
private val absence: String,
private val excusedAbsence: String,
private val schoolAbsence: String,
private val exemption: String,
private val lateness: String,
private val excusedLateness: String
) : AbstractFlexibleItem<AttendanceSummaryItem.ViewHolder>() {
override fun getLayoutRes() = R.layout.item_attendance_summary
override fun createViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>>?): ViewHolder {
return ViewHolder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>?, holder: ViewHolder?, position: Int, payloads: MutableList<Any>?) {
holder?.apply {
attendanceSummaryMonth.text = month
attendanceSummaryPercentage.text = percentage
attendanceSummaryPresent.text = present
attendanceSummaryAbsenceUnexcused.text = absence
attendanceSummaryAbsenceExcused.text = excusedAbsence
attendanceSummaryAbsenceSchool.text = schoolAbsence
attendanceSummaryExemption.text = exemption
attendanceSummaryLatenessUnexcused.text = lateness
attendanceSummaryLatenessExcused.text = excusedLateness
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AttendanceSummaryItem
if (month != other.month) return false
if (percentage != other.percentage) return false
if (present != other.present) return false
if (absence != other.absence) return false
if (excusedAbsence != other.excusedAbsence) return false
if (schoolAbsence != other.schoolAbsence) return false
if (exemption != other.exemption) return false
if (lateness != other.lateness) return false
if (excusedLateness != other.excusedLateness) return false
return true
}
override fun hashCode(): Int {
var result = month.hashCode()
result = 31 * result + percentage.hashCode()
result = 31 * result + present.hashCode()
result = 31 * result + absence.hashCode()
result = 31 * result + excusedAbsence.hashCode()
result = 31 * result + schoolAbsence.hashCode()
result = 31 * result + exemption.hashCode()
result = 31 * result + lateness.hashCode()
result = 31 * result + excusedLateness.hashCode()
return result
}
class ViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>>?) : FlexibleViewHolder(view, adapter),
LayoutContainer {
override val containerView: View?
get() = contentView
}
}

View File

@ -0,0 +1,123 @@
package io.github.wulkanowy.ui.modules.attendance.summary
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.db.entities.Subject
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.SubjectRepostory
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.calculatePercentage
import io.github.wulkanowy.utils.getFormattedName
import io.github.wulkanowy.utils.logEvent
import java.lang.String.format
import java.util.Locale.FRANCE
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject
class AttendanceSummaryPresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val attendanceSummaryRepository: AttendanceSummaryRepository,
private val subjectRepository: SubjectRepostory,
private val studentRepository: StudentRepository,
private val semesterRepository: SemesterRepository,
private val schedulers: SchedulersProvider
) : BasePresenter<AttendanceSummaryView>(errorHandler) {
private var subjects = emptyList<Subject>()
var currentSubjectId = -1
private set
fun onAttachView(view: AttendanceSummaryView, subjectId: Int?) {
super.onAttachView(view)
view.initView()
loadData(subjectId ?: -1)
loadSubjects()
}
fun onSwipeRefresh() {
loadData(currentSubjectId, true)
}
fun onSubjectSelected(name: String) {
view?.run {
showContent(false)
showProgress(true)
clearView()
}
loadData(subjects.singleOrNull { it.name == name }?.realId ?: -1)
}
private fun loadData(subjectId: Int, forceRefresh: Boolean = false) {
currentSubjectId = subjectId
disposable.apply {
clear()
add(studentRepository.getCurrentStudent()
.delay(200, MILLISECONDS)
.flatMap { semesterRepository.getCurrentSemester(it) }
.flatMap { attendanceSummaryRepository.getAttendanceSummary(it, subjectId, forceRefresh) }
.map { createAttendanceSummaryItems(it) to AttendanceSummaryScrollableHeader(formatPercentage(it.calculatePercentage())) }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
view?.run {
hideRefresh()
showProgress(false)
}
}
.subscribe({
view?.apply {
showEmpty(it.first.isEmpty())
showContent(it.first.isNotEmpty())
updateDataSet(it.first, it.second)
}
logEvent("Attendance load", mapOf("forceRefresh" to forceRefresh))
}) {
view?.run { showEmpty(isViewEmpty) }
errorHandler.dispatch(it)
}
)
}
}
private fun loadSubjects() {
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({
view?.run {
view?.updateSubjects(it)
showSubjects(true)
}
}, { errorHandler.dispatch(it) })
)
}
private fun createAttendanceSummaryItems(attendanceSummary: List<AttendanceSummary>): List<AttendanceSummaryItem> {
return attendanceSummary.sortedByDescending { it.id }.map {
AttendanceSummaryItem(
month = it.month.getFormattedName(),
percentage = formatPercentage(it.calculatePercentage()),
present = it.presence.toString(),
absence = it.absence.toString(),
excusedAbsence = it.absenceExcused.toString(),
schoolAbsence = it.absenceForSchoolReasons.toString(),
exemption = it.exemption.toString(),
lateness = it.lateness.toString(),
excusedLateness = it.latenessExcused.toString()
)
}
}
private fun formatPercentage(percentage: Double): String {
return if (percentage == 0.0) "0%"
else "${format(FRANCE, "%.2f", percentage)}%"
}
}

View File

@ -0,0 +1,46 @@
package io.github.wulkanowy.ui.modules.attendance.summary
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.scrollable_header_attendance_summary.*
class AttendanceSummaryScrollableHeader(private val percentage: String) :
AbstractFlexibleItem<AttendanceSummaryScrollableHeader.ViewHolder>() {
override fun getLayoutRes() = R.layout.scrollable_header_attendance_summary
override fun createViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>>?): ViewHolder {
return ViewHolder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>?, holder: ViewHolder?, position: Int, payloads: MutableList<Any>?) {
holder?.apply { attendanceSummaryScrollableHeaderPercentage.text = percentage }
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AttendanceSummaryScrollableHeader
if (percentage != other.percentage) return false
return true
}
override fun hashCode(): Int {
return percentage.hashCode()
}
class ViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>>?) : FlexibleViewHolder(view, adapter),
LayoutContainer {
override val containerView: View?
get() = contentView
}
}

View File

@ -0,0 +1,26 @@
package io.github.wulkanowy.ui.modules.attendance.summary
import io.github.wulkanowy.ui.base.BaseView
interface AttendanceSummaryView : BaseView {
val isViewEmpty: Boolean
fun initView()
fun hideRefresh()
fun showContent(show: Boolean)
fun showProgress(show: Boolean)
fun showEmpty(show: Boolean)
fun updateDataSet(data: List<AttendanceSummaryItem>, header: AttendanceSummaryScrollableHeader)
fun updateSubjects(data: ArrayList<String>)
fun showSubjects(show: Boolean)
fun clearView()
}

View File

@ -78,7 +78,7 @@ class ExamFragment : BaseFragment(), ExamView, MainView.MainChildView, MainView.
}
override fun resetView() {
examAdapter.smoothScrollToPosition(0)
examRecycler.scrollToPosition(0)
}
override fun onFragmentReselected() {

View File

@ -63,8 +63,7 @@ class ExamPresenter @Inject constructor(
if (currentDate != it) {
loadData(it)
reloadView()
view?.resetView()
} else view?.resetView()
} else if (view?.isViewEmpty == false) view?.resetView()
}
}

View File

@ -6,11 +6,12 @@ import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.GradeSummary
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_grade_summary.*
class GradeSummaryItem(header: GradeSummaryHeader, private val grade: String, private val title: String)
: AbstractSectionableItem<GradeSummaryItem.ViewHolder, GradeSummaryHeader>(header) {
class GradeSummaryItem(header: GradeSummaryHeader, private val grade: String, private val title: String) :
AbstractSectionableItem<GradeSummaryItem.ViewHolder, GradeSummaryHeader>(header) {
override fun getLayoutRes() = R.layout.item_grade_summary
@ -18,8 +19,10 @@ class GradeSummaryItem(header: GradeSummaryHeader, private val grade: String, pr
return ViewHolder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>?, holder: ViewHolder?,
position: Int, payloads: MutableList<Any>?) {
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<*>>?, holder: ViewHolder?,
position: Int, payloads: MutableList<Any>?
) {
holder?.run {
gradeSummaryItemGrade.text = grade
gradeSummaryItemTitle.text = title
@ -46,9 +49,8 @@ class GradeSummaryItem(header: GradeSummaryHeader, private val grade: String, pr
return result
}
class ViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>>?)
: FlexibleViewHolder(view, adapter), LayoutContainer {
class ViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>>?) : FlexibleViewHolder(view, adapter),
LayoutContainer {
override val containerView: View?
get() = contentView

View File

@ -11,6 +11,7 @@ import io.github.wulkanowy.ui.modules.about.AboutFragment
import io.github.wulkanowy.ui.modules.about.AboutModule
import io.github.wulkanowy.ui.modules.account.AccountDialog
import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeModule
@ -41,6 +42,10 @@ abstract class MainModule {
@ContributesAndroidInjector
abstract fun bindAttendanceFragment(): AttendanceFragment
@PerFragment
@ContributesAndroidInjector
abstract fun bindAttendanceSummaryFragment(): AttendanceSummaryFragment
@PerFragment
@ContributesAndroidInjector
abstract fun bindExamFragment(): ExamFragment

View File

@ -72,7 +72,7 @@ class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.Mai
get() {
return context?.run {
getString(R.string.about_title) to
ContextCompat.getDrawable(this, R.drawable.ic_more_about_24dp)
ContextCompat.getDrawable(this, R.drawable.ic_all_about_24dp)
}
}

View File

@ -79,7 +79,7 @@ class TimetableFragment : BaseFragment(), TimetableView, MainView.MainChildView,
}
override fun resetView() {
timetableAdapter.smoothScrollToPosition(0)
timetableRecycler.smoothScrollToPosition(0)
}
override fun onFragmentReselected() {

View File

@ -58,7 +58,7 @@ class TimetablePresenter @Inject constructor(
if (currentDate != it) {
loadData(it)
reloadView()
} else view?.resetView()
} else if (view?.isViewEmpty == false) view?.resetView()
}
}

View File

@ -0,0 +1,26 @@
package io.github.wulkanowy.utils
import io.github.wulkanowy.data.db.entities.AttendanceSummary
/**
* [UONET+ - Zasady tworzenia podsumowań liczb uczniów obecnych i nieobecnych w tabeli frekwencji]
* (https://www.vulcan.edu.pl/vulcang_files/user/AABW/AABW-PDF/uonetplus/uonetplus_Frekwencja-liczby-obecnych-nieobecnych.pdf)
*/
private inline val AttendanceSummary.allPresences: Double
get() = presence.toDouble() + absenceForSchoolReasons + lateness + latenessExcused
private inline val AttendanceSummary.allAbsences: Double
get() = absence.toDouble() + absenceExcused
fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences, allAbsences)
fun List<AttendanceSummary>.calculatePercentage(): Double {
return calculatePercentage(sumByDouble { it.allPresences }, sumByDouble { it.allAbsences })
}
private fun calculatePercentage(presence: Double, absence: Double): Double {
return if ((presence + absence) == 0.0) 0.0 else (presence / (presence + absence)) * 100
}

View File

@ -5,6 +5,7 @@ import com.crashlytics.android.answers.CustomEvent
import com.crashlytics.android.answers.LoginEvent
import com.crashlytics.android.answers.SignUpEvent
import timber.log.Timber
import kotlin.math.min
fun logLogin(method: String) {
try {
@ -20,7 +21,7 @@ fun logRegister(message: String, result: Boolean, symbol: String, endpoint: Stri
.putMethod("Login activity")
.putSuccess(result)
.putCustomAttribute("symbol", symbol)
.putCustomAttribute("message", message)
.putCustomAttribute("message", message.substring(0, min(message.length, 100)))
.putCustomAttribute("endpoint", endpoint)
)
} catch (e: Throwable) {

View File

@ -0,0 +1,14 @@
package io.github.wulkanowy.utils
import android.view.View
import android.widget.AdapterView
import android.widget.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)
}
}
}

View File

@ -1,15 +1,22 @@
package io.github.wulkanowy.utils
import org.threeten.bp.DateTimeUtils
import org.threeten.bp.DayOfWeek.*
import org.threeten.bp.DayOfWeek.FRIDAY
import org.threeten.bp.DayOfWeek.MONDAY
import org.threeten.bp.DayOfWeek.SATURDAY
import org.threeten.bp.DayOfWeek.SUNDAY
import org.threeten.bp.Instant
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import org.threeten.bp.Month
import org.threeten.bp.ZoneId
import org.threeten.bp.format.DateTimeFormatter
import org.threeten.bp.format.DateTimeFormatter.ofPattern
import org.threeten.bp.temporal.TemporalAdjusters.*
import java.util.*
import org.threeten.bp.format.TextStyle.FULL_STANDALONE
import org.threeten.bp.temporal.TemporalAdjusters.firstInMonth
import org.threeten.bp.temporal.TemporalAdjusters.next
import org.threeten.bp.temporal.TemporalAdjusters.previous
import java.util.Date
import java.util.Locale
private const val DATE_PATTERN = "dd.MM.yyyy"
@ -29,6 +36,31 @@ fun LocalDate.toFormattedString(format: String = DATE_PATTERN): String = this.fo
fun LocalDateTime.toFormattedString(format: String = DATE_PATTERN): String = this.format(ofPattern(format))
/**
* https://github.com/ThreeTen/threetenbp/issues/55
*/
fun Month.getFormattedName(): String {
return getDisplayName(FULL_STANDALONE, Locale.getDefault())
.let {
when (it) {
"stycznia" -> "Styczeń"
"lutego" -> "Luty"
"marca" -> "Marzec"
"kwietnia" -> "Kwiecień"
"maja" -> "Maj"
"czerwca" -> "Czerwiec"
"lipca" -> "Lipiec"
"sierpnia" -> "Sierpień"
"września" -> "Wrzesień"
"października" -> "Październik"
"listopada" -> "Listopad"
"grudnia" -> "Grudzień"
else -> it
}
}
}
inline val LocalDate.nextSchoolDay: LocalDate
get() {
return when (this.dayOfWeek) {
@ -73,6 +105,7 @@ inline val LocalDate.friday: LocalDate
/**
* [Dz.U. 2016 poz. 1335](http://prawo.sejm.gov.pl/isap.nsf/DocDetails.xsp?id=WDU20160001335)
*/
inline val LocalDate.isHolidays: Boolean
get() {
return LocalDate.of(this.year, 9, 1).run {
@ -82,7 +115,7 @@ inline val LocalDate.isHolidays: Boolean
}
}.let { firstSchoolDay ->
LocalDate.of(this.year, 6, 20)
.with(next(FRIDAY))
.let { lastSchoolDay -> this.isBefore(firstSchoolDay) && this.isAfter(lastSchoolDay) }
.with(next(FRIDAY))
.let { lastSchoolDay -> this.isBefore(firstSchoolDay) && this.isAfter(lastSchoolDay) }
}
}

View File

@ -4,9 +4,9 @@
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#000"
android:fillColor="#FFFFFFFF"
android:pathData="M11,7h2v2h-2zM11,11h2v6h-2z" />
<path
android:fillColor="#000"
android:fillColor="#FFFFFFFF"
android:pathData="M12,2a10,10 0,1 0,0 20,10 10,0 0,0 0,-20zM12,20a8,8 0,1 1,0 -16,8 8,0 0,1 0,16z" />
</vector>

View File

@ -0,0 +1,66 @@
<FrameLayout 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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/attendanceSummarySubjects"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="invisible"
android:background="?android:attr/windowBackground"
android:elevation="5dp"
android:padding="15dp"
android:spinnerMode="dialog" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/attendanceSummarySwipe"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/attendanceSummaryRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
<ProgressBar
android:id="@+id/attendanceSummaryProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
<LinearLayout
android:id="@+id/attendanceSummaryEmpty"
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_attendance_24dp"
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/attendance_no_items"
android:textSize="20sp" />
</LinearLayout>
</FrameLayout>

View File

@ -0,0 +1,252 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/colorControlHighlight"
android:orientation="horizontal"
android:paddingLeft="20dp"
android:paddingTop="7dp"
android:paddingRight="20dp"
android:paddingBottom="7dp">
<TextView
android:id="@+id/attendanceSummaryMonth"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="40dp"
android:layout_marginRight="40dp"
android:layout_weight="1"
android:text="@string/app_name"
android:textSize="17sp" />
<TextView
android:id="@+id/attendanceSummaryPercentage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:text="@string/app_name"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:minHeight="35dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_weight="1"
android:text="@string/attendance_present"
android:textSize="14sp" />
<TextView
android:id="@+id/attendanceSummaryPresent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginEnd="25dp"
android:layout_marginRight="25dp"
android:gravity="end"
android:text="@string/app_name"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:minHeight="35dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_weight="1"
android:text="@string/attendance_absence_unexcused"
android:textSize="14sp" />
<TextView
android:id="@+id/attendanceSummaryAbsenceUnexcused"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginEnd="25dp"
android:layout_marginRight="25dp"
android:gravity="end"
android:text="@string/app_name"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:minHeight="35dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_weight="1"
android:text="@string/attendance_absence_excused"
android:textSize="14sp" />
<TextView
android:id="@+id/attendanceSummaryAbsenceExcused"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginEnd="25dp"
android:layout_marginRight="25dp"
android:gravity="end"
android:text="@string/app_name"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:minHeight="35dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_weight="1"
android:text="@string/attendance_absence_school"
android:textSize="14sp" />
<TextView
android:id="@+id/attendanceSummaryAbsenceSchool"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginEnd="25dp"
android:layout_marginRight="25dp"
android:gravity="end"
android:text="@string/app_name"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:minHeight="35dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_weight="1"
android:text="@string/attendance_exemption"
android:textSize="14sp" />
<TextView
android:id="@+id/attendanceSummaryExemption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginEnd="25dp"
android:layout_marginRight="25dp"
android:gravity="end"
android:text="@string/app_name"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:minHeight="35dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_weight="1"
android:text="@string/attendance_unexcused_lateness"
android:textSize="14sp" />
<TextView
android:id="@+id/attendanceSummaryLatenessUnexcused"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginEnd="25dp"
android:layout_marginRight="25dp"
android:gravity="end"
android:text="@string/app_name"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:minHeight="35dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_weight="1"
android:text="@string/attendance_excused_lateness"
android:textSize="14sp" />
<TextView
android:id="@+id/attendanceSummaryLatenessExcused"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginEnd="25dp"
android:layout_marginRight="25dp"
android:gravity="end"
android:text="@string/app_name"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/attendanceSummaryItemSubject"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="start"
android:maxLines="1"
android:paddingLeft="16dp"
android:paddingTop="12dp"
android:paddingRight="16dp"
android:textAlignment="textStart"
android:paddingBottom="12dp"
android:text="@string/app_name"
android:textSize="16sp" />

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:minLines="2"
android:text="@string/attendance_title"
android:textSize="16sp" />
<TextView
android:id="@+id/attendanceSummaryScrollableHeaderPercentage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="100%"
android:textSize="21sp" />
</LinearLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/attendanceMenuSummary"
android:icon="@drawable/ic_all_about_24dp"
android:orderInCategory="1"
android:title="@string/grade_switch_semester"
app:showAsAction="ifRoom" />
</menu>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/gradeMenuSemester"
android:icon="@drawable/ic_menu_grade_semester_24dp"

View File

@ -118,6 +118,8 @@
<item quantity="many">%1$d nieobecności</item>
</plurals>
<!--Attendance summary-->
<string name="attendance_summary_final">Frekwencja</string>
<!--Exam-->
<string name="exam_no_items">Brak sprawdzianów w tym tygodniu</string>

View File

@ -108,6 +108,9 @@
<item quantity="other">%1$d absences</item>
</plurals>
<!--Attendance summary-->
<string name="attendance_summary_final">Attendance</string>
<!--Exam-->
<string name="exam_no_items">No exams in this week</string>

View File

@ -1,10 +1,13 @@
package io.github.wulkanowy.utils
import org.junit.Assert.*
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import java.util.*
import org.threeten.bp.Month.JANUARY
import java.util.Locale
class TimeExtensionTest {
@ -43,6 +46,14 @@ class TimeExtensionTest {
assertEquals(LocalDate.of(2018, 10, 12), LocalDate.of(2018, 10, 8).friday)
}
@Test
fun monthNameTest() {
Locale.setDefault(Locale.forLanguageTag("PL"))
assertEquals("Styczeń", JANUARY.getFormattedName())
Locale.setDefault(Locale.forLanguageTag("US"))
assertEquals("January", JANUARY.getFormattedName())
}
@Test
fun weekDayNameTest() {
Locale.setDefault(Locale.forLanguageTag("PL"))
@ -74,7 +85,6 @@ class TimeExtensionTest {
assertEquals(LocalDate.of(2018, 10, 3), LocalDate.of(2018, 10, 4).previousSchoolDay)
}
@Test
fun nextOrSameSchoolDayTest() {
assertEquals(LocalDate.of(2018, 9, 28), LocalDate.of(2018, 9, 28).nextOrSameSchoolDay)