diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml index 491de21b..c43a4972 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_build.yml @@ -183,7 +183,7 @@ jobs: run: python $GITHUB_WORKSPACE/.github/utils/webhook_discord.py $GITHUB_WORKSPACE >> $GITHUB_OUTPUT - name: Upload workflow artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: ${{ steps.changelog.outputs.appVersionName }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9227110c..a64a8bdf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,7 @@ name: Release on: push: - tags: ["v*.*.*"] + tags: ["v*.*"] jobs: build: name: Build release (APK) diff --git a/app/src/main/assets/pl-changelog.html b/app/src/main/assets/pl-changelog.html index 173d3877..170f3c05 100644 --- a/app/src/main/assets/pl-changelog.html +++ b/app/src/main/assets/pl-changelog.html @@ -1,8 +1,11 @@ -

Wersja 4.13.7, 2024-07-08

+

Wersja 4.14, 2025-02-02



Dzięki za korzystanie ze Szkolnego!
-© [Kuba Szczodrzyński](@kuba2k2) 2023 +© [Kuba Szczodrzyński](@kuba2k2) 2025 diff --git a/app/src/main/cpp/szkolny-signing.cpp b/app/src/main/cpp/szkolny-signing.cpp index 089e9025..84acb0a3 100644 --- a/app/src/main/cpp/szkolny-signing.cpp +++ b/app/src/main/cpp/szkolny-signing.cpp @@ -9,7 +9,7 @@ /*secret password - removed for source code publication*/ static toys AES_IV[16] = { - 0x0e, 0x87, 0x6d, 0xaa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + 0xee, 0x23, 0xf1, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt index 74e23d88..e89726c6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt @@ -241,6 +241,14 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { withContext(Dispatchers.Default) { config.migrate(this@App) + if (config.appVersionCore < BuildConfig.VERSION_CODE) { + // force syncing all endpoints on update + db.endpointTimerDao().clear() + config.sync.lastAppSync = 0L + config.hash = "invalid" + config.appVersionCore = BuildConfig.VERSION_CODE + } + SSLProviderInstaller.install(applicationContext, this@App::buildHttp) if (config.devModePassword != null) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt index 2042e8b2..eec66c49 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt @@ -43,6 +43,7 @@ class Config(db: AppDb) : BaseConfig(db) { var appInstalledTime by config(0L) var appRateSnackbarTime by config(0L) var appVersion by config(BuildConfig.VERSION_CODE) + var appVersionCore by config(0) var validation by config(null, "buildValidation") var archiverEnabled by config(true) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigGrades.kt index cfe4bc3e..99450da7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigGrades.kt @@ -4,6 +4,7 @@ package pl.szczodrzynski.edziennik.config +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_ECTS import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES @@ -15,8 +16,11 @@ class ProfileConfigGrades(base: ProfileConfig) { var dontCountEnabled by base.config(false) var dontCountGrades by base.config> { listOf() } var hideImproved by base.config(false) + var hideNoGrade by base.config(false) var hideSticksFromOld by base.config(false) var minusValue by base.config(null) var plusValue by base.config(null) var yearAverageMode by base.config(YEAR_ALL_GRADES) + var universityAverageMode by base.config(UNIVERSITY_AVERAGE_MODE_ECTS) + var countEctsInProgress by base.config(false) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt index 44d0fca2..8f75145f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt @@ -114,6 +114,11 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa } } + if (profile?.empty == true) { + // force app sync on first login + app.config.sync.lastAppSync = 0L + } + edziennikInterface = when (loginStore.type) { LoginType.LIBRUS -> Librus(app, profile, loginStore, taskCallback) LoginType.MOBIDZIENNIK -> Mobidziennik(app, profile, loginStore, taskCallback) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt index 8ebafb51..69a5c95c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt @@ -4,6 +4,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos +import com.google.gson.JsonObject import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.api.models.Data import pl.szczodrzynski.edziennik.data.db.entity.LoginStore @@ -73,4 +74,9 @@ class DataUsos( get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 } set(value) { profile["studentId"] = value; mStudentId = value } private var mStudentId: Int? = null + + var termNames: Map = mapOf() + get() { mTermNames = mTermNames ?: profile?.getStudentData("termNames", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mTermNames ?: mapOf() } + set(value) { profile["termNames"] = app.gson.toJson(value); mTermNames = value } + private var mTermNames: Map? = null } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt index f36580fb..2a009af8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt @@ -10,10 +10,12 @@ import pl.szczodrzynski.edziennik.data.db.enums.FeatureType import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod import pl.szczodrzynski.edziennik.data.db.enums.LoginType -const val ENDPOINT_USOS_API_USER = 7000 -const val ENDPOINT_USOS_API_TERMS = 7010 -const val ENDPOINT_USOS_API_COURSES = 7020 -const val ENDPOINT_USOS_API_TIMETABLE = 7030 +const val ENDPOINT_USOS_API_USER = 7000 +const val ENDPOINT_USOS_API_TERMS = 7010 +const val ENDPOINT_USOS_API_COURSES = 7020 +const val ENDPOINT_USOS_API_TIMETABLE = 7030 +const val ENDPOINT_USOS_API_ECTS_POINTS = 7040 +const val ENDPOINT_USOS_API_EXAM_REPORTS = 7050 val UsosFeatures = listOf( /* @@ -39,4 +41,12 @@ val UsosFeatures = listOf( Feature(LoginType.USOS, FeatureType.TIMETABLE, listOf( ENDPOINT_USOS_API_TIMETABLE to LoginMethod.USOS_API, )), + + /* + * Grades + */ + Feature(LoginType.USOS, FeatureType.GRADES, listOf( + ENDPOINT_USOS_API_ECTS_POINTS to LoginMethod.USOS_API, + ENDPOINT_USOS_API_EXAM_REPORTS to LoginMethod.USOS_API, + )), ) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt index dbccb16b..8c85546c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt @@ -8,6 +8,8 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample import pl.szczodrzynski.edziennik.data.api.edziennik.usos.* import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiCourses +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiEctsPoints +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiExamReports 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.data.api.edziennik.usos.data.api.UsosApiUser @@ -58,6 +60,14 @@ class UsosData(val data: DataUsos, val onSuccess: () -> Unit) { data.startProgress(R.string.edziennik_progress_endpoint_timetable) UsosApiTimetable(data, lastSync, onSuccess) } + ENDPOINT_USOS_API_ECTS_POINTS -> { + data.startProgress(R.string.edziennik_progress_endpoint_grade_categories) + UsosApiEctsPoints(data, lastSync, onSuccess) + } + ENDPOINT_USOS_API_EXAM_REPORTS -> { + data.startProgress(R.string.edziennik_progress_endpoint_grades) + UsosApiExamReports(data, lastSync, onSuccess) + } else -> onSuccess(endpointId) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt index e93c63dd..e5086b15 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt @@ -9,6 +9,7 @@ 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_COURSES import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.data.db.entity.GradeCategory import pl.szczodrzynski.edziennik.data.db.entity.Team import pl.szczodrzynski.edziennik.ext.* @@ -25,17 +26,20 @@ class UsosApiCourses( apiRequest( tag = TAG, service = "courses/user", + params = mapOf( + "active_terms_only" to false, + ), fields = listOf( // "terms" to listOf("id", "name", "start_date", "end_date"), "course_editions" to listOf( "course_id", "course_name", - // "term_id", "user_groups" to listOf( "course_unit_id", "group_number", - // "class_type", + "class_type", "class_type_id", + "term_id", "lecturers", ), ), @@ -63,22 +67,38 @@ class UsosApiCourses( for (courseEdition in courseEditions) { val courseId = courseEdition.getString("course_id") ?: 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) { val courseUnitId = userGroup.getLong("course_unit_id") ?: 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 termId = userGroup.getString("term_id") ?: continue val lecturers = userGroup.getLecturerIds("lecturers") - data.teamList.put(courseUnitId, Team( - profileId, - courseUnitId, - "${profile?.studentClassName} $classTypeId$groupNumber - $courseName", - 2, - "${data.schoolId}:${courseId} $classTypeId$groupNumber", - lecturers.firstOrNull() ?: -1L, - )) + data.teamList.put( + courseUnitId, Team( + profileId, + courseUnitId, + "${profile?.studentClassName} $courseName ($classTypeId$groupNumber)", + 2, + "${data.schoolId}:${termId}:${courseId} $classTypeId$groupNumber", + lecturers.firstOrNull() ?: -1L, + ) + ) + + val gradeCategory = data.gradeCategories[courseUnitId] + data.gradeCategories.put( + courseUnitId, GradeCategory( + profileId, + courseUnitId, + gradeCategory?.weight ?: -1.0f, + 0, + courseId, + ).addColumn(classType) + ) + hasValidTeam = true } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiEctsPoints.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiEctsPoints.kt new file mode 100644 index 00000000..b6d112f1 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiEctsPoints.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2025-1-31. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api + +import com.google.gson.JsonObject +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_ECTS_POINTS +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.ext.DAY +import pl.szczodrzynski.edziennik.ext.filter + +class UsosApiEctsPoints( + override val data: DataUsos, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit, +) : UsosApi(data, lastSync) { + companion object { + const val TAG = "UsosApiEctsPoints" + } + + init { + apiRequest( + tag = TAG, + service = "courses/user_ects_points", + responseType = ResponseType.OBJECT, + ) { json, response -> + if (!processResponse(json)) { + data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) + return@apiRequest + } + + data.setSyncNext(ENDPOINT_USOS_API_ECTS_POINTS, 2 * DAY) + onSuccess(ENDPOINT_USOS_API_ECTS_POINTS) + } + } + + private fun processResponse(json: JsonObject): Boolean { + for ((_, coursePointsEl) in json.entrySet()) { + if (!coursePointsEl.isJsonObject) + continue + for ((courseId, pointsEl) in coursePointsEl.asJsonObject.entrySet()) { + if (!pointsEl.isJsonPrimitive) + continue + val gradeCategories = data.gradeCategories + .filter { it.text == courseId } + gradeCategories.forEach { + it.weight = pointsEl.asString.toFloatOrNull() ?: -1.0f + } + } + } + return true + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiExamReports.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiExamReports.kt new file mode 100644 index 00000000..f1e44b16 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiExamReports.kt @@ -0,0 +1,212 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2025-1-31. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api + +import com.google.gson.JsonObject +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_EXAM_REPORTS +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.Grade +import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NORMAL +import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.getBoolean +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.getJsonArray +import pl.szczodrzynski.edziennik.ext.getJsonObject +import pl.szczodrzynski.edziennik.ext.getLong +import pl.szczodrzynski.edziennik.ext.getString +import pl.szczodrzynski.edziennik.ext.join +import pl.szczodrzynski.edziennik.utils.models.Date + +class UsosApiExamReports( + override val data: DataUsos, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit, +) : UsosApi(data, lastSync) { + companion object { + const val TAG = "UsosApiExamReports" + } + + private val missingTermNames = mutableSetOf() + + init { + apiRequest( + tag = TAG, + service = "examrep/user2", + fields = listOf( + "id", + "type_description", + "course_unit" to listOf("id", "course_name"), + "sessions" to listOf( + "number", + "description", + "issuer_grades" to listOf( + "value_symbol", + // "value_description", + "passes", + "counts_into_average", + "date_modified", + "modification_author", + "comment", + ), + ), + ), + responseType = ResponseType.OBJECT, + ) { json, response -> + if (!processResponse(json)) { + data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) + return@apiRequest + } + + data.toRemove.add(DataRemoveModel.Grades.all()) + data.setSyncNext(ENDPOINT_USOS_API_EXAM_REPORTS, SYNC_ALWAYS) + + if (missingTermNames.isEmpty()) + onSuccess(ENDPOINT_USOS_API_EXAM_REPORTS) + else + UsosApiTerms(data, lastSync, onSuccess, missingTermNames) + } + } + + private fun processResponse(json: JsonObject): Boolean { + for ((termId, courseEditionEl) in json.entrySet()) { + if (!courseEditionEl.isJsonObject) + continue + for ((courseId, examReportsEl) in courseEditionEl.asJsonObject.entrySet()) { + if (!examReportsEl.isJsonArray) + continue + for (examReportEl in examReportsEl.asJsonArray) { + if (!examReportEl.isJsonObject) + continue + val examReport = examReportEl.asJsonObject + processExamReport(termId, courseId, examReport) + } + } + } + return true + } + + private fun processExamReport(termId: String, courseId: String, examReport: JsonObject) { + val examId = examReport.getString("id")?.toIntOrNull() + ?: return + val typeDescription = examReport.getLangString("type_description") + val courseUnit = examReport.getJsonObject("course_unit") + ?: return + val courseUnitId = courseUnit.getString("id")?.toLongOrNull() + ?: return + val courseName = courseUnit.getLangString("course_name") + ?: return + val sessions = examReport.getJsonArray("sessions") + ?: return + + val gradeCategory = data.gradeCategories[courseUnitId] + val classType = gradeCategory?.columns?.get(0) + + val subject = data.getSubject( + id = null, + name = courseName, + shortName = courseId, + ) + + var hasGrade = false + + for (sessionEl in sessions) { + if (!sessionEl.isJsonObject) + continue + val session = sessionEl.asJsonObject + + val sessionNumber = session.getInt("number") ?: continue + val sessionDescription = session.getLangString("description") + val issuerGrade = session.getJsonObject("issuer_grades") + + val valueSymbol = issuerGrade.getString("value_symbol") ?: continue + val passes = issuerGrade.getBoolean("passes") + val countsIntoAverage = issuerGrade.getString("counts_into_average") ?: "T" + val dateModified = issuerGrade.getString("date_modified") + val modificationAuthorId = issuerGrade.getJsonObject("modification_author") + ?.getLong("id") ?: -1L + val comment = issuerGrade.getString("comment") + + val value = valueSymbol.toFloatOrNull() ?: 0.0f + + if (termId !in data.termNames) { + missingTermNames.add(termId) + } + + val gradeObject = Grade( + profileId = profileId, + id = examId * 10L + sessionNumber, + name = valueSymbol, + type = TYPE_NORMAL, + value = value, + weight = if (countsIntoAverage == "T") gradeCategory?.weight ?: 0.0f else 0.0f, + color = (if (passes == true) 0xFF465FB3 else 0xFFB71C1C).toInt(), + category = typeDescription, + description = listOfNotNull(classType, sessionDescription, comment).join(" - "), + comment = termId, + semester = 1, + teacherId = modificationAuthorId, + subjectId = subject.id, + addedDate = Date.fromIso(dateModified), + ) + hasGrade = true + + if (sessionNumber > 1) { + val origId = examId * 10L + sessionNumber - 1 + val grades = data.gradeList.filter { it.id == origId } + val improvedGrade = grades.firstOrNull() + improvedGrade?.parentId = gradeObject.id + improvedGrade?.weight = 0.0f + gradeObject.isImprovement = true + } + + data.gradeList.add(gradeObject) + data.metadataList.add( + Metadata( + profileId, + MetadataType.GRADE, + gradeObject.id, + profile?.empty ?: false, + profile?.empty ?: false, + ) + ) + } + + if (!hasGrade) { + // add an "empty" grade for the exam + val gradeObject = Grade( + profileId = profileId, + id = examId * 10L, + name = "...", + type = TYPE_NO_GRADE, + value = 0.0f, + weight = 0.0f, + color = 0xFFBABABD.toInt(), + category = typeDescription, + description = classType, + comment = termId, + semester = 1, + teacherId = -1L, + subjectId = subject.id, + addedDate = 0, + ) + data.gradeList.add(gradeObject) + data.metadataList.add( + Metadata( + profileId, + MetadataType.GRADE, + gradeObject.id, + true, + true, + ) + ) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt index b1b2fd88..93da3aea 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt @@ -5,6 +5,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api 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.edziennik.usos.DataUsos import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS @@ -16,43 +17,81 @@ class UsosApiTerms( override val data: DataUsos, override val lastSync: Long?, val onSuccess: (endpointId: Int) -> Unit, + names: Set? = null, ) : UsosApi(data, lastSync) { companion object { const val TAG = "UsosApiTerms" } init { - apiRequest( - tag = TAG, - service = "terms/search", - params = mapOf( - "query" to Date.getToday().year.toString(), - ), - responseType = ResponseType.ARRAY, - ) { json, response -> - if (!processResponse(json)) { - data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) - return@apiRequest - } + if (names != null) { + apiRequest( + tag = TAG, + service = "terms/terms", + params = mapOf("term_ids" to names.joinToString("|")), + responseType = ResponseType.OBJECT, + ) { json, response -> + if (!processResponse(json.entrySet().map { it.value.asJsonObject })) { + data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) + return@apiRequest + } - data.setSyncNext(ENDPOINT_USOS_API_TERMS, 7 * DAY) - onSuccess(ENDPOINT_USOS_API_TERMS) + data.setSyncNext(ENDPOINT_USOS_API_TERMS, 2 * DAY) + onSuccess(ENDPOINT_USOS_API_TERMS) + } + } else { + apiRequest( + tag = TAG, + service = "terms/search", + params = mapOf("query" to Date.getToday().year.toString()), + responseType = ResponseType.ARRAY, + ) { json, response -> + if (!processResponse(json.asJsonObjectList())) { + data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) + return@apiRequest + } + + data.setSyncNext(ENDPOINT_USOS_API_TERMS, 2 * DAY) + onSuccess(ENDPOINT_USOS_API_TERMS) + } } } - private fun processResponse(json: JsonArray): Boolean { + private fun processResponse(terms: List): Boolean { + val profile = profile ?: return false + val termNames = data.termNames.toMutableMap() val today = Date.getToday() - for (term in json.asJsonObjectList()) { + for (term in terms) { + val id = term.getString("id") + val name = term.getLangString("name") + val orderKey = term.getInt("order_key") + if (id != null && name != null) + termNames[id] = "$orderKey$$name" + if (!term.getBoolean("is_active", false)) continue val startDate = term.getString("start_date")?.let { Date.fromY_m_d(it) } ?: continue val finishDate = term.getString("finish_date")?.let { Date.fromY_m_d(it) } ?: continue - if (today in startDate..finishDate) { - profile?.studentSchoolYearStart = startDate.year - profile?.dateSemester1Start = startDate - profile?.dateSemester2Start = finishDate - } + if (today !in startDate..finishDate) + continue + + if (startDate.month >= 8) + profile.dateSemester1Start = startDate + else + profile.dateSemester2Start = startDate + + if (finishDate.month >= 8) + profile.dateYearEnd = finishDate + else + profile.dateSemester2Start = finishDate } + // update school year start + profile.studentSchoolYearStart = profile.dateSemester1Start.year + // update year end date if there is a new year + if (profile.dateYearEnd <= profile.dateSemester1Start) + profile.dateYearEnd = + profile.dateSemester1Start.clone().setYear(profile.dateSemester1Start.year + 1) + data.termNames = termNames return true } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt index 29a3e22b..0e3808f8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt @@ -32,7 +32,8 @@ class UsosApiUser( "last_name", "student_number", "student_programmes" to listOf( - "programme" to listOf("id"), + "id", + "programme" to listOf("id", "description"), ), ), ), @@ -40,9 +41,11 @@ class UsosApiUser( ) { json, response -> val programmes = json.getJsonArray("student_programmes") if (programmes.isNullOrEmpty()) { - data.error(ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES) - .withApiResponse(json) - .withResponse(response)) + data.error( + ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES) + .withApiResponse(json) + .withResponse(response) + ) return@apiRequest } @@ -50,13 +53,19 @@ class UsosApiUser( val lastName = json.getString("last_name") val studentName = buildFullName(firstName, lastName) + val studentProgrammeId = programmes.getJsonObject(0) + .getString("id") + val programmeId = programmes.getJsonObject(0) + .getJsonObject("programme") + .getString("id") + data.studentId = json.getInt("id") ?: data.studentId profile?.studentNameLong = studentName profile?.studentNameShort = studentName.getShortName() profile?.studentNumber = json.getInt("student_number", -1) - profile?.studentClassName = programmes.getJsonObject(0).getJsonObject("programme").getString("id") + profile?.studentClassName = programmeId - profile?.studentClassName?.let { + val team = programmeId?.let { data.getTeam( id = null, name = it, @@ -64,6 +73,7 @@ class UsosApiUser( isTeamClass = true, ) } + team?.code = "${data.schoolId}:${studentProgrammeId}:${programmeId}" data.setSyncNext(ENDPOINT_USOS_API_USER, 4 * DAY) onSuccess(ENDPOINT_USOS_API_USER) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt index 954d6bb1..2f3bf634 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt @@ -206,7 +206,7 @@ class SzkolnyApi(val app: App) : CoroutineScope { teams.filter { it.profileId == profile.id }.map { it.code } ) val hash = user.toString().md5() - if (hash == profile.config.hash) + if (hash == profile.config.hash && app.config.hash != "invalid") return@mapNotNull null return@mapNotNull user to profile.config } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt index f1c69abd..95e5e0a9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt @@ -46,6 +46,6 @@ object Signing { /*fun provideKey(param1: String, param2: Long): ByteArray {*/ fun pleaseStopRightNow(param1: String, param2: Long): ByteArray { - return "$param1.MTIzNDU2Nzg5MD0WAYwfGc===.$param2".sha256() + return "$param1.MTIzNDU2Nzg5MDADAoYzGn===.$param2".sha256() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EndpointTimerDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EndpointTimerDao.kt index c0ff0464..512218da 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EndpointTimerDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EndpointTimerDao.kt @@ -24,4 +24,7 @@ interface EndpointTimerDao { @Query("DELETE FROM endpointTimers WHERE profileId = :profileId") fun clear(profileId: Int) + + @Query("DELETE FROM endpointTimers") + fun clear() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Grade.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Grade.kt index 5bf3bd86..5e54dc61 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Grade.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Grade.kt @@ -55,6 +55,7 @@ open class Grade( const val TYPE_DESCRIPTIVE = 30 const val TYPE_DESCRIPTIVE_TEXT = 31 const val TYPE_TEXT = 40 + const val TYPE_NO_GRADE = 100 } @ColumnInfo(name = "gradeValueMax") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/LoginTypeFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/LoginTypeFeatures.kt index b0d16002..f5eee20f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/LoginTypeFeatures.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/LoginTypeFeatures.kt @@ -93,6 +93,7 @@ internal val FEATURES_PODLASIE = setOf( internal val FEATURES_USOS = setOf( TIMETABLE, AGENDA, + GRADES, STUDENT_INFO, STUDENT_NUMBER, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/ProfileExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/ProfileExtensions.kt index 186a28bd..f13a88a2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/ProfileExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/ProfileExtensions.kt @@ -72,8 +72,7 @@ fun Profile.getAppData() = if (App.profileId == this.id) App.data else AppData.get(this.loginStoreType) fun Profile.shouldArchive(): Boolean { - if (loginStoreType == LoginType.DEMO) - return false + return false // vulcan hotfix if (dateYearEnd.month > 6) { dateYearEnd.month = 6 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/GradesConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/GradesConfigDialog.kt index b1c0c86f..a20c1c26 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/GradesConfigDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/GradesConfigDialog.kt @@ -21,6 +21,8 @@ import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_M import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_DATE_DESC import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_SUBJECT_ASC +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_ECTS +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_SIMPLE import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_AVG import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_SEM import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_SEM_2_AVG @@ -47,6 +49,8 @@ class GradesConfigDialog( @SuppressLint("SetTextI18n") override suspend fun loadConfig() { + b.isUniversity = app.gradesManager.isUniversity + b.customPlusCheckBox.isChecked = app.profile.config.grades.plusValue != null b.customPlusValue.isVisible = b.customPlusCheckBox.isChecked b.customMinusCheckBox.isChecked = app.profile.config.grades.minusValue != null @@ -76,10 +80,18 @@ class GradesConfigDialog( else -> null }?.isChecked = true + when (app.profile.config.grades.universityAverageMode) { + UNIVERSITY_AVERAGE_MODE_ECTS -> b.gradeUniversityAverageMode1 + UNIVERSITY_AVERAGE_MODE_SIMPLE -> b.gradeUniversityAverageMode0 + else -> null + }?.isChecked = true + b.dontCountGrades.isChecked = app.profile.config.grades.dontCountEnabled && app.profile.config.grades.dontCountGrades.isNotEmpty() b.hideImproved.isChecked = app.profile.config.grades.hideImproved + b.hideNoGrade.isChecked = app.profile.config.grades.hideNoGrade b.averageWithoutWeight.isChecked = app.profile.config.grades.averageWithoutWeight + b.countEctsInProgress.isChecked = app.profile.config.grades.countEctsInProgress if (app.profile.config.grades.dontCountGrades.isEmpty()) { b.dontCountGradesText.setText("nb, 0, bz, bd") @@ -149,10 +161,21 @@ class GradesConfigDialog( app.profile.config.grades.yearAverageMode = YEAR_1_SEM_2_SEM } + b.gradeUniversityAverageMode1.setOnSelectedListener { + app.profile.config.grades.universityAverageMode = UNIVERSITY_AVERAGE_MODE_ECTS + } + b.gradeUniversityAverageMode0.setOnSelectedListener { + app.profile.config.grades.universityAverageMode = UNIVERSITY_AVERAGE_MODE_SIMPLE + } + b.hideImproved.onChange { _, isChecked -> app.profile.config.grades.hideImproved = isChecked } + b.hideNoGrade.onChange { _, isChecked -> app.profile.config.grades.hideNoGrade = isChecked } b.averageWithoutWeight.onChange { _, isChecked -> app.profile.config.grades.averageWithoutWeight = isChecked } + b.countEctsInProgress.onChange { _, isChecked -> + app.profile.config.grades.countEctsInProgress = isChecked + } b.averageWithoutWeightHelp.onClick { MaterialAlertDialogBuilder(activity) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesAdapter.kt index 05bb1da0..3979903d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesAdapter.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Grade +import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE import pl.szczodrzynski.edziennik.data.db.full.GradeFull import pl.szczodrzynski.edziennik.ext.onClick import pl.szczodrzynski.edziennik.ext.startCoroutineTimer @@ -134,6 +135,7 @@ class GradesAdapter( if (model.state == STATE_CLOSED) { val subItems = when { + model is GradesSubject && manager.isUniversity -> listOf() model is GradesSemester && model.grades.isEmpty() -> listOf(GradesEmpty()) model is GradesSemester && manager.hideImproved -> @@ -147,10 +149,12 @@ class GradesAdapter( if (notifyAdapter) notifyItemInserted(position) } - position++ model.state = STATE_OPENED - items.addAll(position, subItems.filterNotNull()) - if (notifyAdapter) notifyItemRangeInserted(position, subItems.size) + if (subItems.isNotEmpty()) { + position++ + items.addAll(position, subItems.filterNotNull()) + if (notifyAdapter) notifyItemRangeInserted(position, subItems.size) + } if (model is GradesSubject) { // auto expand first semester @@ -232,10 +236,13 @@ class GradesAdapter( } } - if (item !is GradeFull || onGradeClick != null) + if (item !is GradeFull || (onGradeClick != null && item.type != TYPE_NO_GRADE)) { holder.itemView.setOnClickListener(onClickListener) - else + holder.itemView.isEnabled = true + } else { holder.itemView.setOnClickListener(null) + holder.itemView.isEnabled = false + } } fun notifyItemChanged(model: Any) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesListFragment.kt index 905abefd..80e7a06c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesListFragment.kt @@ -18,6 +18,7 @@ import com.mikepenz.iconics.typeface.library.community.material.CommunityMateria import kotlinx.coroutines.* import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Grade +import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.full.GradeFull import pl.szczodrzynski.edziennik.databinding.GradesListFragmentBinding @@ -28,7 +29,10 @@ import pl.szczodrzynski.edziennik.ui.grades.models.GradesAverages import pl.szczodrzynski.edziennik.ui.grades.models.GradesSemester import pl.szczodrzynski.edziennik.ui.grades.models.GradesStats import pl.szczodrzynski.edziennik.ui.grades.models.GradesSubject +import pl.szczodrzynski.edziennik.utils.TextInputDropDown import pl.szczodrzynski.edziennik.utils.managers.GradesManager +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_ECTS +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_SIMPLE import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem import kotlin.coroutines.CoroutineContext @@ -85,6 +89,35 @@ class GradesListFragment : Fragment(), CoroutineScope { else -> grades } + if (manager.isUniversity) { + val termIds = grades.map { it.comment }.toSet().toMutableList() + val termNames: MutableMap = mutableMapOf() + // deserialize to a map of termId to (orderKey, termName) + val terms = app.profile.getStudentData("termNames", null) + ?.let { app.gson.fromJson(it, termNames::class.java) } + ?.mapValues { (_, value) -> value.split('$', limit = 2) } + ?.mapValues { (_, value) -> Pair(value[0].toIntOrNull() ?: 0, value[1]) } + ?: mapOf() + // sort by order key + termIds.sortByDescending { termId -> terms[termId]?.first ?: 0 } + // populate the dropdown + b.semesterLayout.isVisible = true + b.semesterDropdown.items = termIds.mapIndexed { id, termId -> + TextInputDropDown.Item( + id.toLong(), + terms[termId]?.second ?: termId ?: "-", + tag = termId, + ) + }.toMutableList() + b.semesterDropdown.select(index = 0) + b.semesterDropdown.setOnChangeListener { item -> + b.semesterDropdown.select(item) + adapter.items = processGrades(items) + adapter.notifyDataSetChanged() + return@setOnChangeListener true + } + } + // load & configure the adapter adapter.items = withContext(Dispatchers.Default) { processGrades(items) } if (items.isNotNullNorEmpty() && b.list.adapter == null) { @@ -188,13 +221,23 @@ class GradesListFragment : Fragment(), CoroutineScope { var semesterNumber = 0 var subject = GradesSubject(subjectId, "") var semester = GradesSemester(0, 1) + val isUniversity = manager.isUniversity + val filterTermId = b.semesterDropdown.selected?.tag - val hideImproved = manager.hideImproved + val hideNoGrade = app.profile.config.grades.hideNoGrade + val countEctsInProgress = app.profile.config.grades.countEctsInProgress + val universityAverageMode = app.profile.config.grades.universityAverageMode // grades returned by the query are ordered // by the subject ID, so it's easier and probably // a bit faster to build all the models for (grade in grades) { + if (isUniversity && filterTermId != null && grade.comment != filterTermId) + continue + + if (hideNoGrade && grade.type == TYPE_NO_GRADE) + continue + /*if (grade.parentId != null && grade.parentId != -1L) continue // the grade is hidden as a new, improved one is available*/ if (grade.subjectId != subjectId) { @@ -258,8 +301,63 @@ class GradesListFragment : Fragment(), CoroutineScope { subject.lastAddedDate = max(subject.lastAddedDate, grade.addedDate) } + when (manager.orderBy) { + GradesManager.ORDER_BY_DATE_DESC -> items.sortByDescending { it.lastAddedDate } + GradesManager.ORDER_BY_DATE_ASC -> items.sortBy { it.lastAddedDate } + } + val stats = GradesStats() + if (isUniversity) { + val semesterSum = mutableListOf() + val semesterCount = mutableListOf() + val totalSum = mutableListOf() + val totalCount = mutableListOf() + val ectsPoints = mutableMapOf, Float>() + for (grade in grades) { + val pointsPair = grade.subjectId to grade.comment + if (grade.type == TYPE_NO_GRADE && !countEctsInProgress) + // reset points if there's an exam that isn't passed yet + ectsPoints[pointsPair] = 0.0f + + if (grade.value == 0.0f || grade.weight == 0.0f) + continue + if (universityAverageMode == UNIVERSITY_AVERAGE_MODE_ECTS) + totalSum.add(grade.value * grade.weight) + else + totalSum.add(grade.value) + totalCount.add(grade.weight) + + if (grade.value < 3.0) + // exam not passed, reset points for this subject + ectsPoints[pointsPair] = 0.0f + else if (pointsPair !in ectsPoints) + // no points for this subject, simply assign + ectsPoints[pointsPair] = grade.weight + + if (filterTermId != null && grade.comment != filterTermId) + continue + + if (universityAverageMode == UNIVERSITY_AVERAGE_MODE_ECTS) + semesterSum.add(grade.value * grade.weight) + else + semesterSum.add(grade.value) + semesterCount.add(grade.weight) + } + when (universityAverageMode) { + UNIVERSITY_AVERAGE_MODE_SIMPLE -> { + stats.universitySem = semesterSum.sum() / semesterCount.size + stats.universityTotal = totalSum.sum() / totalCount.size + } + UNIVERSITY_AVERAGE_MODE_ECTS -> { + stats.universitySem = semesterSum.sum() / semesterCount.sum() + stats.universityTotal = totalSum.sum() / totalCount.sum() + } + } + stats.universityEcts = ectsPoints.values.sum() + return (items + stats).toMutableList() + } + val sem1Expected = mutableListOf() val sem2Expected = mutableListOf() val yearlyExpected = mutableListOf() @@ -330,11 +428,6 @@ class GradesListFragment : Fragment(), CoroutineScope { stats.pointSem2 = sem2Point.averageOrNull()?.toFloat() ?: 0f stats.pointYearly = yearlyPoint.averageOrNull()?.toFloat() ?: 0f - when (manager.orderBy) { - GradesManager.ORDER_BY_DATE_DESC -> items.sortByDescending { it.lastAddedDate } - GradesManager.ORDER_BY_DATE_ASC -> items.sortBy { it.lastAddedDate } - } - return (items + stats).toMutableList() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesStats.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesStats.kt index 26041dcd..2d8f04e9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesStats.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesStats.kt @@ -22,4 +22,8 @@ class GradesStats { var pointSem1 = 0f var pointSem2 = 0f var pointYearly = 0f + + var universitySem = 0f + var universityTotal = 0f + var universityEcts = 0f } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/GradeViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/GradeViewHolder.kt index 85684886..7c53880e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/GradeViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/GradeViewHolder.kt @@ -11,6 +11,7 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE import pl.szczodrzynski.edziennik.data.db.full.GradeFull import pl.szczodrzynski.edziennik.databinding.GradesItemGradeBinding import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter @@ -59,9 +60,12 @@ class GradeViewHolder( b.gradeWeight.isVisible = weightText != null b.gradeTeacherName.text = grade.teacherName - b.gradeAddedDate.text = Date.fromMillis(grade.addedDate).let { - it.getRelativeString(app, 5) ?: it.formattedStringShort - } + if (grade.addedDate == 0L || grade.type == TYPE_NO_GRADE) + b.gradeAddedDate.text = null + else + b.gradeAddedDate.text = Date.fromMillis(grade.addedDate).let { + it.getRelativeString(app, 5) ?: it.formattedStringShort + } b.unread.isVisible = grade.showAsUnseen if (!grade.seen) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/StatsViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/StatsViewHolder.kt index 3386b276..c0c821e2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/StatsViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/StatsViewHolder.kt @@ -31,9 +31,35 @@ class StatsViewHolder( override fun onBind(activity: AppCompatActivity, app: App, item: GradesStats, position: Int, adapter: GradesAdapter) { val manager = app.gradesManager + val isUniversity = manager.isUniversity val showAverages = mutableListOf() val showPoint = mutableListOf() + b.universityTitle.isVisible = isUniversity + b.universityLayout.isVisible = isUniversity + b.universityDivider.isVisible = isUniversity + + if (isUniversity) { + val format = DecimalFormat("#.00") + + b.normalTitle.isVisible = false + b.normalLayout.isVisible = false + b.normalDivider.isVisible = false + b.helpButton.isVisible = false + b.pointTitle.isVisible = false + b.pointLayout.isVisible = false + b.pointDivider.isVisible = false + b.noData.isVisible = false + b.disclaimer.isVisible = true + b.customValueDivider.isVisible = false + b.customValueLayout.isVisible = false + + b.universitySemester.text = format.format(item.universitySem) + b.universityTotal.text = format.format(item.universityTotal) + b.universityEcts.text = format.format(item.universityEcts) + return + } + getSemesterString(app, item.normalSem1, item.normalSem1Proposed, item.normalSem1Final, item.sem1NotAllFinal).let { (average, notice) -> b.normalSemester1Layout.isVisible = average != null b.normalSemester1Notice.isVisible = notice != null diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/SubjectViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/SubjectViewHolder.kt index 1033fd65..51ea92ef 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/SubjectViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/SubjectViewHolder.kt @@ -62,9 +62,24 @@ class SubjectViewHolder( val firstSemester = item.semesters.firstOrNull() ?: return - b.yearSummary.text = manager.getYearSummaryString(app, item.semesters.map { it.grades.size }.sum(), item.averages) + if (manager.isUniversity) { + val ectsPoints = item.semesters.firstOrNull()?.grades?.maxOf { it.weight } + b.yearSummary.text = if (ectsPoints != null) + contextWrapper.getString( + R.string.grades_ects_points_format, + ectsPoints + ) + else + null + } else { + b.yearSummary.text = manager.getYearSummaryString( + app, + item.semesters.map { it.grades.size }.sum(), + item.averages + ) + } - if (firstSemester.number != item.semester) { + if (firstSemester.number != item.semester && !manager.isUniversity) { b.gradesContainer.addView(TextView(contextWrapper).apply { setTextColor(android.R.attr.textColorSecondary.resolveAttr(context)) setText(R.string.grades_preview_other_semester, firstSemester.number) @@ -92,16 +107,18 @@ class SubjectViewHolder( )) } - b.previewContainer.addView(TextView(contextWrapper).apply { - setTextColor(android.R.attr.textColorSecondary.resolveAttr(context)) - text = manager.getAverageString(app, firstSemester.averages, nameSemester = true, showSemester = firstSemester.number) - //gravity = Gravity.END - layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { - setMargins(0, 0, 8.dp, 0) - } - maxLines = 1 - ellipsize = TextUtils.TruncateAt.END - }) + if (!manager.isUniversity) { + b.previewContainer.addView(TextView(contextWrapper).apply { + setTextColor(android.R.attr.textColorSecondary.resolveAttr(context)) + text = manager.getAverageString(app, firstSemester.averages, nameSemester = true, showSemester = firstSemester.number) + //gravity = Gravity.END + layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { + setMargins(0, 0, 8.dp, 0) + } + maxLines = 1 + ellipsize = TextUtils.TruncateAt.END + }) + } // add the topmost semester's grades to preview container (collapsed) firstSemester.proposedGrade?.let { @@ -139,7 +156,7 @@ class SubjectViewHolder( } // if showing semester 2, add yearly grades to preview container (collapsed) - if (firstSemester.number == item.semester) { + if (firstSemester.number == item.semester && !manager.isUniversity) { b.previewContainer.addView(TextView(contextWrapper).apply { text = manager.getAverageString(app, item.averages, nameSemester = true) layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeGradesCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeGradesCard.kt index c64f2cc3..0d8d7e47 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeGradesCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeGradesCard.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Subject import pl.szczodrzynski.edziennik.data.db.full.GradeFull @@ -70,7 +71,7 @@ class HomeGradesCard( app.db.gradeDao().getAllFromDate(profile.id, sevenDaysAgo).observe(fragment, Observer { grades.apply { clear() - addAll(it) + addAll(it.filter { it.type != TYPE_NO_GRADE }) } update() }) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/GradesManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/GradesManager.kt index bfce3067..e4e59730 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/GradesManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/GradesManager.kt @@ -12,9 +12,11 @@ import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Grade import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NORMAL +import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_POINT_AVG import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_POINT_SUM import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_YEAR_FINAL +import pl.szczodrzynski.edziennik.data.db.enums.SchoolType import pl.szczodrzynski.edziennik.data.db.full.GradeFull import pl.szczodrzynski.edziennik.ext.asColoredSpannable import pl.szczodrzynski.edziennik.ext.get @@ -42,6 +44,8 @@ class GradesManager(val app: App) : CoroutineScope { const val YEAR_ALL_GRADES = 4 const val COLOR_MODE_DEFAULT = 0 const val COLOR_MODE_WEIGHTED = 1 + const val UNIVERSITY_AVERAGE_MODE_SIMPLE = 0 + const val UNIVERSITY_AVERAGE_MODE_ECTS = 1 } private val job = Job() @@ -69,6 +73,8 @@ class GradesManager(val app: App) : CoroutineScope { get() = app.profile.config.grades.hideImproved val averageWithoutWeight get() = app.profile.config.grades.averageWithoutWeight + val isUniversity + get() = app.profile.loginStoreType.schoolType == SchoolType.UNIVERSITY fun getOrderByString() = when (orderBy) { @@ -87,6 +93,7 @@ class GradesManager(val app: App) : CoroutineScope { else context.getString(R.string.grades_weight_format, format.format(grade.weight)) TYPE_POINT_AVG -> context.getString(R.string.grades_max_points_format, format.format(grade.valueMax)) + TYPE_NO_GRADE -> context.getString(R.string.grades_weight_no_grade) else -> null } @@ -158,6 +165,19 @@ class GradesManager(val app: App) : CoroutineScope { } } type == TYPE_NORMAL && defColor -> grade.color and 0xffffff + type == TYPE_NORMAL && app.profile.loginStoreType.schoolType == SchoolType.UNIVERSITY -> { + when (grade.name.lowercase()) { + "zal" -> 0x4caf50 + "nb", "nk" -> 0xff7043 + "2.0", "nzal" -> 0xff3d00 + "3.0" -> 0xffff00 + "3.5" -> 0xc6ff00 + "4.0" -> 0x76ff03 + "4.5" -> 0x64dd17 + "5.0" -> 0x00c853 + else -> grade.color and 0xffffff + } + } type in TYPE_NORMAL..TYPE_YEAR_FINAL -> { when (grade.name.lowercase()) { "+", "++", "+++" -> 0x4caf50 diff --git a/app/src/main/res/layout/dialog_config_grades.xml b/app/src/main/res/layout/dialog_config_grades.xml index 1e0e9058..76f6d2ae 100644 --- a/app/src/main/res/layout/dialog_config_grades.xml +++ b/app/src/main/res/layout/dialog_config_grades.xml @@ -6,6 +6,14 @@ + + + + + + @@ -19,16 +27,18 @@ + android:orientation="horizontal" + android:isVisible="@{!isUniversity}"> + android:orientation="horizontal" + android:isVisible="@{!isUniversity}"> + android:background="@drawable/divider" + android:isVisible="@{!isUniversity}" /> + android:text="@string/grades_config_dont_count_grades" + android:isVisible="@{!isUniversity}" /> + android:enabled="@{dontCountGrades.checked}" + android:isVisible="@{!isUniversity}"> + + + android:orientation="horizontal" + android:isVisible="@{!isUniversity}"> + + + android:text="@string/menu_grades_average_mode" + android:isVisible="@{!isUniversity}" /> + android:layout_height="wrap_content" + android:isVisible="@{!isUniversity}"> + + + + + + + + + diff --git a/app/src/main/res/layout/grades_item_stats.xml b/app/src/main/res/layout/grades_item_stats.xml index 8bddb7de..84c05383 100644 --- a/app/src/main/res/layout/grades_item_stats.xml +++ b/app/src/main/res/layout/grades_item_stats.xml @@ -283,6 +283,105 @@ android:layout_marginTop="8dp" android:background="@drawable/divider" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + android:orientation="vertical"> + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9731a603..79c9cfb2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -445,9 +445,12 @@ Oceny oddziel przecinkiem Podaj oceny… Ukrywaj oceny poprawione z listy + Licz ECTS przed zaliczeniem całego przedmiotu + Ukrywaj pozycje \"brak oceny\" Własna wartość minusa Własna wartość plusa Konfiguracja ocen + Punkty ECTS: %#.2f Dodaj ocenę Dodawanie oceny Podaj wagę oceny @@ -474,9 +477,11 @@ semestr %d: %spkt Semestr %d Semestr %d + wszystkie oceny Aktualne ustawienia ocen mogą wpływać na średnią. Jeśli uważasz, że się ona nie zgadza, kliknij Konfiguruj. Została ustawiona własna wartość plusa/minusa. Jeśli uważasz, że się ona nie zgadza, kliknij Konfiguruj. *średnie ocen są poglądowe i mogą się różnić, w zależności od ustawień szkoły + punkty ECTS *przewidywana średnia *z ocen końcowych\nPrzewidywana: %s *z ocen końcowych @@ -490,11 +495,14 @@ Śr. ocen proponowanych:\n%s semestr 1 semestr 2 + ten semestr Statystyka ocen + Średnia ocen za studia całoroczna wartość: %s waga %s nie liczona do śr. + brak oceny koniec roku: %s koniec roku: %s%% koniec roku: %spkt @@ -689,6 +697,7 @@ Ustawienia ocen Symulator edycji ocen Sortuj oceny + Sposób obliczania średniej za studia Pomoc Strona główna Zadania domowe @@ -970,6 +979,8 @@ Nie wliczaj oceny 0 do średniej Ten e-dziennik nie został jeszcze zaimplementowany w aplikacji. Pokazuj nieobecności nauczycieli w Terminarzu + Średnia arytmetyczna ocen + Średnia ważona ocen (ECTS = waga) Tablica ogłoszeń Obecności/nieobecności Dni wolne klasy diff --git a/build.gradle b/build.gradle index 89b20995..ac7e6962 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { kotlin_version = '1.6.10' release = [ - versionName: "4.13.7", - versionCode: 4130799 + versionName: "4.14", + versionCode: 4140099 ] setup = [