[API/Usos] Implement Timetable.

This commit is contained in:
Kuba Szczodrzyński 2022-10-16 17:21:36 +02:00
parent 8d174bda01
commit 4de066bf5f
No known key found for this signature in database
GPG Key ID: 70CB8A85BA1633CB
7 changed files with 188 additions and 27 deletions

View File

@ -5,25 +5,35 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.usos package pl.szczodrzynski.edziennik.data.api.edziennik.usos
import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.FEATURE_ALWAYS_NEEDED
import pl.szczodrzynski.edziennik.data.api.FEATURE_STUDENT_INFO
import pl.szczodrzynski.edziennik.data.api.FEATURE_TEAM_INFO
import pl.szczodrzynski.edziennik.data.api.models.Feature import pl.szczodrzynski.edziennik.data.api.models.Feature
const val ENDPOINT_USOS_API_USER = 7000 const val ENDPOINT_USOS_API_USER = 7000
const val ENDPOINT_USOS_API_TERMS = 7010 const val ENDPOINT_USOS_API_TERMS = 7010
const val ENDPOINT_USOS_API_COURSES = 7020 const val ENDPOINT_USOS_API_COURSES = 7020
const val ENDPOINT_USOS_API_TIMETABLE = 7030
val UsosFeatures = listOf( val UsosFeatures = listOf(
/*
* Student information
*/
Feature(LOGIN_TYPE_USOS, FEATURE_STUDENT_INFO, listOf( Feature(LOGIN_TYPE_USOS, FEATURE_STUDENT_INFO, listOf(
ENDPOINT_USOS_API_USER to LOGIN_METHOD_USOS_API, ENDPOINT_USOS_API_USER to LOGIN_METHOD_USOS_API,
), listOf(LOGIN_METHOD_USOS_API)), ), listOf(LOGIN_METHOD_USOS_API)),
/*
* Terms & courses
*/
Feature(LOGIN_TYPE_USOS, FEATURE_SCHOOL_INFO, listOf( Feature(LOGIN_TYPE_USOS, FEATURE_SCHOOL_INFO, listOf(
ENDPOINT_USOS_API_TERMS to LOGIN_METHOD_USOS_API, ENDPOINT_USOS_API_TERMS to LOGIN_METHOD_USOS_API,
), listOf(LOGIN_METHOD_USOS_API)), ), listOf(LOGIN_METHOD_USOS_API)),
Feature(LOGIN_TYPE_USOS, FEATURE_TEAM_INFO, listOf( Feature(LOGIN_TYPE_USOS, FEATURE_TEAM_INFO, listOf(
ENDPOINT_USOS_API_COURSES to LOGIN_METHOD_USOS_API, ENDPOINT_USOS_API_COURSES to LOGIN_METHOD_USOS_API,
), listOf(LOGIN_METHOD_USOS_API)), ), listOf(LOGIN_METHOD_USOS_API)),
/*
* Timetable
*/
Feature(LOGIN_TYPE_USOS, FEATURE_TIMETABLE, listOf(
ENDPOINT_USOS_API_TIMETABLE to LOGIN_METHOD_USOS_API,
), listOf(LOGIN_METHOD_USOS_API)),
) )

View File

@ -6,12 +6,10 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos import pl.szczodrzynski.edziennik.data.api.edziennik.usos.*
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_COURSES
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_USER
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiCourses import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiCourses
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTerms import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTerms
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTimetable
import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.Utils.d
class UsosData(val data: DataUsos, val onSuccess: () -> Unit) { class UsosData(val data: DataUsos, val onSuccess: () -> Unit) {
@ -55,6 +53,10 @@ class UsosData(val data: DataUsos, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_teams) data.startProgress(R.string.edziennik_progress_endpoint_teams)
UsosApiCourses(data, lastSync, onSuccess) UsosApiCourses(data, lastSync, onSuccess)
} }
ENDPOINT_USOS_API_TIMETABLE -> {
data.startProgress(R.string.edziennik_progress_endpoint_timetable)
UsosApiTimetable(data, lastSync, onSuccess)
}
else -> onSuccess(endpointId) else -> onSuccess(endpointId)
} }
} }

View File

@ -29,12 +29,12 @@ class UsosApiCourses(
// "terms" to listOf("id", "name", "start_date", "end_date"), // "terms" to listOf("id", "name", "start_date", "end_date"),
"course_editions" to listOf( "course_editions" to listOf(
"course_id", "course_id",
// "course_name", "course_name",
// "term_id", // "term_id",
"user_groups" to listOf( "user_groups" to listOf(
"course_unit_id", "course_unit_id",
"group_number", "group_number",
"class_type", // "class_type",
"class_type_id", "class_type_id",
// "lecturers", // "lecturers",
), ),
@ -62,18 +62,18 @@ class UsosApiCourses(
var hasValidTeam = false var hasValidTeam = false
for (courseEdition in courseEditions) { for (courseEdition in courseEditions) {
val courseId = courseEdition.getString("course_id") ?: continue val courseId = courseEdition.getString("course_id") ?: continue
// val courseName = courseEdition.getLangString("course_name") ?: continue val courseName = courseEdition.getLangString("course_name") ?: continue
val userGroups = courseEdition.getJsonArray("user_groups")?.asJsonObjectList() ?: continue val userGroups = courseEdition.getJsonArray("user_groups")?.asJsonObjectList() ?: continue
for (userGroup in userGroups) { for (userGroup in userGroups) {
val courseUnitId = userGroup.getLong("course_unit_id") ?: continue val courseUnitId = userGroup.getLong("course_unit_id") ?: continue
val groupNumber = userGroup.getInt("group_number") ?: continue val groupNumber = userGroup.getInt("group_number") ?: continue
val classType = userGroup.getLangString("class_type") ?: continue // val classType = userGroup.getLangString("class_type") ?: continue
val classTypeId = userGroup.getString("class_type_id") ?: continue val classTypeId = userGroup.getString("class_type_id") ?: continue
data.teamList.put(courseUnitId, Team( data.teamList.put(courseUnitId, Team(
profileId, profileId,
courseUnitId, courseUnitId,
"$classType $groupNumber ($courseId)", "${profile?.studentClassName} $classTypeId$groupNumber - $courseName",
2, 2,
"${data.schoolId}:${courseId} $classTypeId$groupNumber", "${data.schoolId}:${courseId} $classTypeId$groupNumber",
-1, -1,

View File

@ -5,7 +5,6 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS
@ -32,7 +31,7 @@ class UsosApiTerms(
responseType = ResponseType.ARRAY, responseType = ResponseType.ARRAY,
) { json, response -> ) { json, response ->
if (!processResponse(json)) { if (!processResponse(json)) {
data.error(UsosApiCourses.TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
return@apiRequest return@apiRequest
} }

View File

@ -0,0 +1,132 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-16.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
import com.google.gson.JsonArray
import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TIMETABLE
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.models.Week
class UsosApiTimetable(
override val data: DataUsos,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit,
) : UsosApi(data, lastSync) {
companion object {
const val TAG = "UsosApiTimetable"
}
init {
val currentWeekStart = Week.getWeekStart()
if (Date.getToday().weekDay > 4)
currentWeekStart.stepForward(0, 0, 7)
val weekStart = data.arguments
?.getString("weekStart")
?.let { Date.fromY_m_d(it) }
?: currentWeekStart
val weekEnd = weekStart.clone().stepForward(0, 0, 6)
apiRequest<JsonArray>(
tag = TAG,
service = "tt/user",
params = mapOf(
"start" to weekStart.stringY_m_d,
"days" to 7,
),
fields = listOf(
"type",
"start_time",
"end_time",
"unit_id",
"course_id",
"course_name",
"lecturer_ids",
"building_id",
"room_number",
"classtype_id",
"group_number",
),
responseType = ResponseType.ARRAY,
) { json, response ->
if (!processResponse(json, weekStart..weekEnd)) {
data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
return@apiRequest
}
data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd))
data.setSyncNext(ENDPOINT_USOS_API_TIMETABLE, SYNC_ALWAYS)
onSuccess(ENDPOINT_USOS_API_TIMETABLE)
}
}
private fun processResponse(json: JsonArray, syncRange: ClosedRange<Date>): Boolean {
val foundDates = mutableSetOf<Date>()
for (activity in json.asJsonObjectList()) {
val type = activity.getString("type")
if (type !in listOf("classgroup", "classgroup2"))
continue
val startTime = activity.getString("start_time") ?: continue
val endTime = activity.getString("end_time") ?: continue
val unitId = activity.getLong("unit_id", -1)
val courseName = activity.getLangString("course_name") ?: continue
val courseId = activity.getString("course_id") ?: continue
val lecturerIds = activity.getJsonArray("lecturer_ids")?.map { it.asLong }
val buildingId = activity.getString("building_id")
val roomNumber = activity.getString("room_number")
val classTypeId = activity.getString("classtype_id")
val groupNumber = activity.getString("group_number")
val lesson = Lesson(profileId, -1).also {
it.type = Lesson.TYPE_NORMAL
it.date = Date.fromY_m_d(startTime)
it.startTime = Time.fromY_m_d_H_m_s(startTime)
it.endTime = Time.fromY_m_d_H_m_s(endTime)
it.subjectId = data.getSubject(
id = null,
name = courseName,
shortName = courseId,
).id
it.teacherId = lecturerIds?.firstOrNull() ?: -1L
it.teamId = unitId
val groupName = classTypeId?.plus(groupNumber)?.let { s -> "($s)" }
it.classroom = "$buildingId / $roomNumber ${groupName ?: ""}"
it.id = it.buildId()
}
lesson.date?.let { foundDates += it }
val seen = profile?.empty != false || lesson.date!! < Date.getToday()
data.lessonList.add(lesson)
if (lesson.type != Lesson.TYPE_NORMAL)
data.metadataList += Metadata(
profileId,
Metadata.TYPE_LESSON_CHANGE,
lesson.id,
seen,
seen,
)
}
val notFoundDates = syncRange.asSequence() - foundDates
for (date in notFoundDates) {
data.lessonList += Lesson(profileId, date.value.toLong()).also {
it.type = Lesson.TYPE_NO_LESSONS
it.date = date
}
}
return true
}
}

View File

@ -7,9 +7,10 @@ package pl.szczodrzynski.edziennik.ext
import android.content.Context import android.content.Context
import im.wangchao.mhttp.Response import im.wangchao.mhttp.Response
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.Locale
const val MINUTE = 60L const val MINUTE = 60L
const val HOUR = 60L*MINUTE const val HOUR = 60L*MINUTE
@ -115,3 +116,11 @@ fun Context.getSyncInterval(interval: Int): String {
"" ""
return hoursText?.plus(" $minutesText") ?: minutesText return hoursText?.plus(" $minutesText") ?: minutesText
} }
fun ClosedRange<Date>.asSequence(): Sequence<Date> = sequence {
val date = this@asSequence.start.clone()
while (date in this@asSequence) {
yield(date.clone())
date.stepForward(0, 0, 1)
}
}

View File

@ -21,8 +21,11 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.* import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_USOS
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
@ -40,8 +43,8 @@ import pl.szczodrzynski.edziennik.utils.managers.NoteManager
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.mutableLazy import pl.szczodrzynski.edziennik.utils.mutableLazy
import java.util.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.math.max
import kotlin.math.min import kotlin.math.min
class TimetableDayFragment : LazyFragment(), CoroutineScope { class TimetableDayFragment : LazyFragment(), CoroutineScope {
@ -82,7 +85,7 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
startHour = startHour, startHour = startHour,
endHour = endHour, endHour = endHour,
dividerHeight = 1.dp, dividerHeight = 1.dp,
halfHourHeight = 60.dp, halfHourHeight = if (app.profile.loginStoreType == LOGIN_TYPE_USOS) 45.dp else 30.dp,
hourDividerColor = R.attr.hourDividerColor.resolveAttr(context), hourDividerColor = R.attr.hourDividerColor.resolveAttr(context),
halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context), halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context),
hourLabelWidth = 40.dp, hourLabelWidth = 40.dp,
@ -184,11 +187,18 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
val lessonsActual = lessons.filter { it.type != Lesson.TYPE_NO_LESSONS } val lessonsActual = lessons.filter { it.type != Lesson.TYPE_NO_LESSONS }
val minStartHour = lessonsActual.minOf { it.displayStartTime?.hour ?: DEFAULT_END_HOUR }
val maxEndHour = lessonsActual.maxOf { it.displayEndTime?.hour?.plus(1) ?: DEFAULT_START_HOUR }
if (profileConfig.timetableTrimHourRange) { if (profileConfig.timetableTrimHourRange) {
dayViewDelegate.deinitialize() dayViewDelegate.deinitialize()
// end/start defaults are swapped on purpose // end/start defaults are swapped on purpose
startHour = lessonsActual.minOf { it.displayStartTime?.hour ?: DEFAULT_END_HOUR } startHour = minStartHour
endHour = lessonsActual.maxOf { it.displayEndTime?.hour?.plus(1) ?: DEFAULT_START_HOUR } endHour = maxEndHour
} else if (startHour > minStartHour || endHour < maxEndHour) {
dayViewDelegate.deinitialize()
startHour = min(startHour, minStartHour)
endHour = max(endHour, maxEndHour)
} }
b.scrollView.isVisible = true b.scrollView.isVisible = true
@ -377,9 +387,8 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
// The day view needs the event time ranges in the start minute/end minute format, // The day view needs the event time ranges in the start minute/end minute format,
// so calculate those here // so calculate those here
val startMinute = 60 * (lesson.displayStartTime?.hour val startMinute = 60 * startTime.hour + startTime.minute
?: 0) + (lesson.displayStartTime?.minute ?: 0) val endMinute = 60 * endTime.hour + endTime.minute
val endMinute = startMinute + 45
eventTimeRanges.add(DayView.EventTimeRange(startMinute, endMinute)) eventTimeRanges.add(DayView.EventTimeRange(startMinute, endMinute))
} }