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
- - Dodano opcję uruchomienia aplikacji bez logowania.
+ - USOS: dodano obsługę ocen.
+ - USOS: obliczanie średniej za studia oraz punktów ECTS.
+ - USOS: poprawiono brak planu zajęć po rozpoczęciu roku.
+ - Wyłączono archiwizator profili.
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 = [