From 4f75a327aa88a1773ce468aab0743923284b6406 Mon Sep 17 00:00:00 2001 From: Tobias Jungmann <32565407+tobiasjungmann@users.noreply.github.com> Date: Thu, 28 Jul 2022 14:51:55 +0200 Subject: [PATCH] Average grade improvement (#1439) * Added ui to enter grade parameters * Added menu item to enter edit mode * Added click listener to edit menu item to toggle edit mode * toggeling edit mode changes now all ui elements * Exam List is stored as a json object in shared preferences * connected ui to the model - exams are saved permanently in shared preferences * connected checkbox to the exam model * reordered ui elements for better visibility * extracted string ressources * downloading new grades update the existing grade list * hidden grades are o displayed correctly after initialization * added ui to manually add exams * added ui to delete manually added exams * added logic to delete manually added exams * added logic to manually add exams * reduced number of store operations, newly added exams are now directly shown * finished manually adding and deleting exams * started input sanitization, tried to fix recycler overwrites * improved input sanitization for remaining attributes * adapted computation of the average grade * prevented wrong updates of credits in the recycler view * fixed ui inconsistencies, reduced method sizes * persistently stored exams in shared preferences * extracted string resources * linted small warnings * fixed styling issue with the add grade dialog, added option for weighted grades in the diagrams. * fixed unclickable checkbox * fixed inconsistency in saving exams to the shared preferences * changed stickyListHeaders import to a still maintained project to prevent occasionally crashes on closing * added translations, linted; show program spinner only if more than one program exits * added more intuitive icons, fixed translation * Added ui to enter grade parameters * Added menu item to enter edit mode * Added click listener to edit menu item to toggle edit mode * toggeling edit mode changes now all ui elements * Exam List is stored as a json object in shared preferences * connected ui to the model - exams are saved permanently in shared preferences * connected checkbox to the exam model * reordered ui elements for better visibility * extracted string ressources * downloading new grades update the existing grade list * hidden grades are o displayed correctly after initialization * added ui to manually add exams * added ui to delete manually added exams * added logic to delete manually added exams * added logic to manually add exams * reduced number of store operations, newly added exams are now directly shown * finished manually adding and deleting exams * started input sanitization, tried to fix recycler overwrites * improved input sanitization for remaining attributes * adapted computation of the average grade * prevented wrong updates of credits in the recycler view * fixed ui inconsistencies, reduced method sizes * persistently stored exams in shared preferences * extracted string resources * linted small warnings * fixed styling issue with the add grade dialog, added option for weighted grades in the diagrams. * fixed unclickable checkbox * fixed inconsistency in saving exams to the shared preferences * added translations, linted; show program spinner only if more than one program exits * Fix Build (#1436) * Fix ktlint errors * Help out Kotlins type inference * Move long test strings from code to seperate json files * Bump Robolectric version to 4.8 * Update appcompat, fragment libraries and fix findViewById() in Fragments (#1434) * Update appcompat, fragment and fix findViewById() in Fragments * fix formatting Co-authored-by: Kordian Bruck * Release 3.18 * added more intuitive icons, fixed translation * fixed errors from merging * added questionmark to text * Update app/src/main/java/de/tum/in/tumcampusapp/component/tumui/grades/ExamListAdapter.kt Co-authored-by: Fabian Sauter * Apply suggestions from code review Co-authored-by: Fabian Sauter * Apply suggestions from code review in grades Fragment Co-authored-by: Fabian Sauter * implemented requested feedback * linted grade files Co-authored-by: Christian <9384305+ctsk@users.noreply.github.com> Co-authored-by: Jakob Foerste Co-authored-by: Kordian Bruck Co-authored-by: Kordian Bruck Co-authored-by: Fabian Sauter --- .../in/tumcampusapp/api/app/DateSerializer.kt | 17 +- .../tumonline/converters/DateTimeConverter.kt | 8 +- .../component/tumui/grades/ExamListAdapter.kt | 191 +++++++- .../component/tumui/grades/GradesFragment.kt | 460 ++++++++++++++---- .../grades/GradesNotificationProvider.kt | 2 +- .../component/tumui/grades/model/Exam.kt | 109 +++-- .../component/tumui/grades/model/ExamList.kt | 2 +- .../main/res/drawable/grade_background.xml | 2 +- .../main/res/drawable/ic_baseline_save_24.xml | 5 + .../main/res/layout-land/activity_grades.xml | 97 ---- .../main/res/layout-land/fragment_grades.xml | 227 +++++---- .../res/layout/activity_grades_listview.xml | 136 +++++- .../res/layout/dialog_add_grade_input.xml | 164 +++++++ app/src/main/res/layout/fragment_grades.xml | 168 ++++--- .../main/res/menu/menu_activity_grades.xml | 7 + app/src/main/res/values-de/strings.xml | 31 +- app/src/main/res/values/strings.xml | 31 +- 17 files changed, 1223 insertions(+), 434 deletions(-) create mode 100644 app/src/main/res/drawable/ic_baseline_save_24.xml create mode 100644 app/src/main/res/layout/dialog_add_grade_input.xml diff --git a/app/src/main/java/de/tum/in/tumcampusapp/api/app/DateSerializer.kt b/app/src/main/java/de/tum/in/tumcampusapp/api/app/DateSerializer.kt index fa8fde769b..4a76ebcbf7 100644 --- a/app/src/main/java/de/tum/in/tumcampusapp/api/app/DateSerializer.kt +++ b/app/src/main/java/de/tum/in/tumcampusapp/api/app/DateSerializer.kt @@ -14,9 +14,14 @@ import java.util.* */ class DateSerializer : JsonDeserializer, JsonSerializer { private val formatStrings = arrayOf( - "yyyy-MM-dd", - "yyyy-MM-dd HH:mm:ss", - "yyyy-MM-dd'T'HH:mm:ss'Z'" + "dd.MM.yy", + "dd.MM.yyyy", + "yyyy.MM.dd", + "dd-MM-yy", + "dd-MM-yyyy", + "yyyy-MM-dd", + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd'T'HH:mm:ss'Z'" ) private val dateFormats = formatStrings.map { @@ -37,7 +42,11 @@ class DateSerializer : JsonDeserializer, JsonSerializer { throw JsonParseException("Unparseable date: \"${json?.asString.orEmpty()}\". Supported formats: ${formatStrings.contentToString()}") } - override fun serialize(time: DateTime, typeOfT: Type, context: JsonSerializationContext): JsonElement { + override fun serialize( + time: DateTime, + typeOfT: Type, + context: JsonSerializationContext + ): JsonElement { return JsonPrimitive(DateTimeUtils.getDateString(time)) } } \ No newline at end of file diff --git a/app/src/main/java/de/tum/in/tumcampusapp/api/tumonline/converters/DateTimeConverter.kt b/app/src/main/java/de/tum/in/tumcampusapp/api/tumonline/converters/DateTimeConverter.kt index c3b80602cb..fdfd27729e 100644 --- a/app/src/main/java/de/tum/in/tumcampusapp/api/tumonline/converters/DateTimeConverter.kt +++ b/app/src/main/java/de/tum/in/tumcampusapp/api/tumonline/converters/DateTimeConverter.kt @@ -9,9 +9,11 @@ import org.joda.time.format.DateTimeFormat class DateTimeConverter : TypeConverter { private val formats = arrayOf( - "yyyy-MM-dd", - "yyyy-MM-dd HH:mm", - "yyyy-MM-dd HH:mm:ss" + "dd.MM.yy", + "dd.MM.yyyy", + "yyyy-MM-dd", + "yyyy-MM-dd HH:mm", + "yyyy-MM-dd HH:mm:ss" ) override fun read(value: String?): DateTime? { diff --git a/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/grades/ExamListAdapter.kt b/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/grades/ExamListAdapter.kt index f62395fec2..6a90606c44 100644 --- a/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/grades/ExamListAdapter.kt +++ b/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/grades/ExamListAdapter.kt @@ -4,6 +4,13 @@ import android.content.Context import android.view.View import android.view.ViewGroup import android.widget.TextView +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.CheckBox +import android.widget.Button +import android.widget.ImageView +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat import de.tum.`in`.tumcampusapp.R import de.tum.`in`.tumcampusapp.component.other.generic.adapter.SimpleStickyListHeadersAdapter import de.tum.`in`.tumcampusapp.component.tumui.grades.model.Exam @@ -12,7 +19,10 @@ import org.joda.time.format.DateTimeFormat /** * Custom UI adapter for a list of exams. */ -class ExamListAdapter(context: Context, results: List) : SimpleStickyListHeadersAdapter(context, results.toMutableList()) { +class ExamListAdapter(context: Context, results: List, gradesFragment: GradesFragment) : + SimpleStickyListHeadersAdapter(context, results.toMutableList()) { + + val localGradesFragment: GradesFragment = gradesFragment init { itemList.sort() @@ -32,24 +42,175 @@ class ExamListAdapter(context: Context, results: List) : SimpleStickyListH } val exam = itemList[position] - holder.nameTextView.text = exam.course - holder.gradeTextView.text = exam.grade - val gradeColor = exam.getGradeColor(context) - holder.gradeTextView.background.setTint(gradeColor) + initUIEditElements(holder, exam) + initUIDisplayElements(holder, exam) + return view + } + private fun initUIDisplayElements(holder: ViewHolder, exam: Exam) { + holder.nameTextView.text = exam.course + holder.gradeTextView.text = exam.grade + adaptUIToCheckboxStatus(holder, exam) val date: String = if (exam.date == null) { context.getString(R.string.not_specified) } else { DATE_FORMAT.print(exam.date) } - holder.examDateTextView.text = String.format("%s: %s", context.getString(R.string.date), date) + holder.examDateTextView.text = + String.format("%s: %s", context.getString(R.string.date), date) - holder.additionalInfoTextView.text = String.format("%s: %s, %s: %s", - context.getString(R.string.examiner), exam.examiner, - context.getString(R.string.mode), exam.modus) + holder.additionalInfoTextView.text = String.format( + "%s: %s, %s: %s", + context.getString(R.string.examiner), exam.examiner, + context.getString(R.string.mode), exam.modus + ) + } - return view + /** + * Init the ui Elements to change the parameters of the grade + */ + private fun initUIEditElements(holder: ViewHolder, exam: Exam) { + if (localGradesFragment.isEditModeEnabled()) { + holder.editGradesContainer.visibility = View.GONE + holder.gradeTextViewDeleteCustomGrade.visibility = View.GONE + } else { + holder.editGradesContainer.visibility = View.VISIBLE + + initListenerDeleteCustomGrade(exam, holder) + initListenerEditTexts(exam, holder) + initListenerResetGradeParameters(exam, holder) + initCheckBoxUsedInAverage(exam, holder) + } + } + + /** + * Adds a ClickListener which will show a confirmation dialog whether the exam should actually be deleted. + */ + private fun initListenerDeleteCustomGrade(exam: Exam, holder: ViewHolder) { + if (exam.manuallyAdded) { + holder.gradeTextViewDeleteCustomGrade.visibility = View.VISIBLE + + holder.gradeTextViewDeleteCustomGrade.setOnClickListener { + val dialog = AlertDialog.Builder(localGradesFragment.requireContext()) + .setTitle(context.getString(R.string.delete_exam)) + .setMessage( + context.getString(R.string.delete_exam_dialog_message) + ) + .setPositiveButton(R.string.delete) { _, _ -> + + localGradesFragment.deleteExamFromList(exam) + } + .setNegativeButton(android.R.string.cancel, null) + .create() + .apply { + window?.setBackgroundDrawableResource(R.drawable.rounded_corners_background) + } + dialog.show() + dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor( + localGradesFragment.resources.getColor(R.color.text_primary) + ) + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor( + localGradesFragment.resources.getColor(R.color.text_primary) + ) + } + } else { + holder.gradeTextViewDeleteCustomGrade.visibility = View.GONE + } + } + + /** + * Adds on Focus change listeners which store the value to the exam object if and only if the + * user finished editing the exam. + */ + private fun initListenerEditTexts(exam: Exam, holder: ViewHolder) { + holder.editTextGradeWeights.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + val oldWeight = holder.editTextGradeWeights.text.toString().toDouble() + if (exam.weight != oldWeight) { + exam.weight = oldWeight + localGradesFragment.storeExamListInSharedPreferences() + notifyDataSetChanged() + } + } + } + + holder.editTextGradeCredits.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + val oldCredits = holder.editTextGradeCredits.text.toString().toInt() + if (exam.credits_new != oldCredits) { + exam.credits_new = oldCredits + localGradesFragment.storeExamListInSharedPreferences() + notifyDataSetChanged() + } + } + } + holder.editTextGradeWeights.setText(exam.weight.toString()) + holder.editTextGradeCredits.setText(exam.credits_new.toString()) + } + + /** + * Adds a ClickListener to reset one exam to the default values and adapts the UI accordingly. + */ + private fun initListenerResetGradeParameters(exam: Exam, holder: ViewHolder) { + holder.buttonResetGradeParameters.setOnClickListener(object : View.OnClickListener { + override fun onClick(p0: View?) { + exam.gradeUsedInAverage = true + adaptUIToCheckboxStatus(holder, exam) + holder.checkBoxUseGradeForAverage.isChecked = true + exam.weight = 1.0 + exam.credits_new = 6 + holder.editTextGradeWeights.setText(exam.weight.toString()) + holder.editTextGradeCredits.setText(exam.credits_new.toString()) + localGradesFragment.storeExamListInSharedPreferences() + notifyDataSetChanged() + } + }) + } + + /** + * Initializes the state of the checkbox and adapts the UI accordingly. + */ + private fun initCheckBoxUsedInAverage(exam: Exam, holder: ViewHolder) { + holder.checkBoxUseGradeForAverage.isChecked = exam.gradeUsedInAverage + adaptUIToCheckboxStatus(holder, exam) + holder.checkBoxUseGradeForAverage.setOnClickListener() { + exam.gradeUsedInAverage = holder.checkBoxUseGradeForAverage.isChecked + adaptUIToCheckboxStatus(holder, exam) + localGradesFragment.storeExamListInSharedPreferences() + notifyDataSetChanged() + } + } + + /** + * Enables/disables Edittexts, and adapts the color of the grade bar on the right side. + */ + private fun adaptUIToCheckboxStatus( + holder: ViewHolder, + exam: Exam + ) { + if (exam.gradeUsedInAverage) { + holder.editTextGradeCredits.isEnabled = true + holder.editTextGradeWeights.isEnabled = true + val gradeColor = exam.getGradeColor(context) + holder.gradeTextView.background.setTint(gradeColor) + holder.gradeTextView.setTextColor(ContextCompat.getColor(context, R.color.white)) + } else { + holder.editTextGradeCredits.isEnabled = false + holder.editTextGradeWeights.isEnabled = false + holder.gradeTextView.background.setTint( + ContextCompat.getColor( + context, + R.color.transparent + ) + ) + holder.gradeTextView.setTextColor( + ContextCompat.getColor( + context, + R.color.grade_default + ) + ) + } } override fun generateHeaderName(item: Exam): String { @@ -71,6 +232,16 @@ class ExamListAdapter(context: Context, results: List) : SimpleStickyListH var gradeTextView: TextView = itemView.findViewById(R.id.gradeTextView) var examDateTextView: TextView = itemView.findViewById(R.id.examDateTextView) var additionalInfoTextView: TextView = itemView.findViewById(R.id.additionalInfoTextView) + + var editTextGradeWeights: EditText = itemView.findViewById(R.id.editTextGradeWeight) + var editTextGradeCredits: EditText = itemView.findViewById(R.id.editTextCreditsOfSubject) + var editGradesContainer: LinearLayout = itemView.findViewById(R.id.editGradesContainer) + var checkBoxUseGradeForAverage: CheckBox = + itemView.findViewById(R.id.checkBoxUseGradeForAverage) + val buttonResetGradeParameters: Button = + itemView.findViewById(R.id.buttonResetGradeParameters) + val gradeTextViewDeleteCustomGrade: ImageView = + itemView.findViewById(R.id.gradeTextViewDeleteCustomGrade) } companion object { diff --git a/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/grades/GradesFragment.kt b/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/grades/GradesFragment.kt index b59fe15af8..d0482896c3 100644 --- a/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/grades/GradesFragment.kt +++ b/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/grades/GradesFragment.kt @@ -2,23 +2,25 @@ package de.tum.`in`.tumcampusapp.component.tumui.grades import android.animation.Animator import android.animation.AnimatorListenerAdapter -import android.content.res.Configuration +import android.content.Context import android.graphics.Color import android.os.Bundle import android.util.ArrayMap -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View +import android.util.Log +import android.view.* import android.widget.AdapterView import android.widget.ArrayAdapter -import androidx.core.app.ActivityCompat.recreate +import android.widget.Button +import android.widget.EditText +import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat.getColor import com.github.mikephil.charting.components.Legend import com.github.mikephil.charting.components.LegendEntry import com.github.mikephil.charting.data.* import com.github.mikephil.charting.formatter.ValueFormatter +import com.google.gson.* +import com.google.gson.reflect.TypeToken import com.zhuinden.fragmentviewbindingdelegatekt.viewBinding import de.tum.`in`.tumcampusapp.R import de.tum.`in`.tumcampusapp.api.tumonline.CacheControl @@ -29,12 +31,17 @@ import de.tum.`in`.tumcampusapp.databinding.FragmentGradesBinding import de.tum.`in`.tumcampusapp.utils.Const import de.tum.`in`.tumcampusapp.utils.Utils import org.jetbrains.anko.support.v4.defaultSharedPreferences +import org.joda.time.DateTime +import org.joda.time.format.DateTimeFormat +import org.joda.time.format.DateTimeFormatter +import java.lang.reflect.Type import java.text.NumberFormat -import java.util.* +import java.util.Locale +import javax.inject.Inject class GradesFragment : FragmentForAccessingTumOnline( - R.layout.fragment_grades, - R.string.my_grades + R.layout.fragment_grades, + R.string.my_grades ) { private var spinnerPosition = 0 @@ -45,7 +52,11 @@ class GradesFragment : FragmentForAccessingTumOnline( private var showBarChartAfterRotate = false - private var currentOrientation: Int? = null + private var globalEditOFF = true + private var adaptDiagramToWeights = true + private val exams = mutableListOf() + + private val examSharedPreferences: String = "ExamList" private val grades: Array by lazy { resources.getStringArray(R.array.grades) @@ -68,8 +79,6 @@ class GradesFragment : FragmentForAccessingTumOnline( showBarChartAfterRotate = !state.getBoolean(KEY_SHOW_BAR_CHART, true) spinnerPosition = state.getInt(KEY_SPINNER_POSITION, 0) } - - currentOrientation = resources.configuration.orientation } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -86,6 +95,11 @@ class GradesFragment : FragmentForAccessingTumOnline( showListButton?.setOnClickListener { toggleInLandscape() } showChartButton?.setOnClickListener { toggleInLandscape() } + initUIVisibility() + } + binding.floatingButtonAddExamGrade.setOnClickListener { openAddGradeDialog() } + binding.checkboxUseDiagrams.setOnCheckedChangeListener { _, isChecked -> + adaptDiagramToWeights = isChecked } loadGrades(CacheControl.USE_CACHE) @@ -105,8 +119,13 @@ class GradesFragment : FragmentForAccessingTumOnline( } override fun onDownloadSuccessful(response: ExamList) { - val exams = response.exams.orEmpty() + val examsDownloaded: MutableList = response.exams.orEmpty().toMutableList() + loadExamListFromSharedPreferences() + addAllNewItemsToExamList(examsDownloaded) + initUIAfterDownloadingExams() + } + private fun initUIAfterDownloadingExams() { initSpinner(exams) showExams(exams) @@ -119,6 +138,99 @@ class GradesFragment : FragmentForAccessingTumOnline( storeGradedCourses(exams) } + private fun addExamToList(exam: Exam) { + exams.add(exam) + changeNumberOfExams() + } + + fun deleteExamFromList(exam: Exam) { + exams.remove(exam) + changeNumberOfExams() + } + + private fun changeNumberOfExams() { + binding.gradesListView.adapter = ExamListAdapter(requireContext(), exams, this) + storeExamListInSharedPreferences() + } + + /** + * Adds all exams which are part of the new list to the existing exams list. + */ + private fun addAllNewItemsToExamList(examsDownloaded: MutableList) { + val examsTitles = exams.map { it.course } + examsDownloaded.removeAll { examsTitles.contains(it.course) } + + if (examsDownloaded.isNotEmpty()) { + examsDownloaded.forEach { + it.credits_new = 5 + it.weight = 1.0 + it.gradeUsedInAverage = true + } + exams.addAll(examsDownloaded) + storeExamListInSharedPreferences() + } + } + + private fun loadExamListFromSharedPreferences() { + try { + val sharedPref = activity?.getPreferences(Context.MODE_PRIVATE) ?: return + val dateTimeConverter = DateTimeConverter() + val gson = GsonBuilder().registerTypeAdapter(DateTime::class.java, dateTimeConverter) + .create() + val listType = object : TypeToken>() {}.type + val jsonString = sharedPref.getString(examSharedPreferences, "") + if (jsonString != null && jsonString != "[]") { + exams.clear() + exams.addAll(gson.fromJson(jsonString, listType)) + return + } + } catch (e: Exception) { + Log.v("Adding Exams", "Error while loading exams. Exam list was cleared.") + exams.clear() + } + } + + fun storeExamListInSharedPreferences() { + val sharedPref = activity?.getPreferences(Context.MODE_PRIVATE) ?: return + val dateTimeConverter = DateTimeConverter() + val gson = GsonBuilder().registerTypeAdapter(DateTime::class.java, dateTimeConverter) + .create() + val jsonlist = gson.toJson(exams) + with(sharedPref.edit()) { + putString(examSharedPreferences, jsonlist) + apply() + } + } + + /** + * Gson serialiser/deserialiser for converting Joda [DateTime] objects. + * Source: https://riptutorial.com/android/example/14799/adding-a-custom-converter-to-gson + */ + class DateTimeConverter @Inject constructor() : JsonSerializer, + JsonDeserializer { + private val dateTimeFormatter: DateTimeFormatter = + DateTimeFormat.forPattern("YYYY-MM-dd HH:mm") + + override fun serialize( + src: DateTime?, + typeOfSrc: Type?, + context: JsonSerializationContext? + ): JsonElement { + return JsonPrimitive(dateTimeFormatter.print(src)) + } + + @Throws(JsonParseException::class) + override fun deserialize( + json: JsonElement, + typeOfT: Type?, + context: JsonDeserializationContext? + ): DateTime? { + return if (json.asString == null || json.asString.isEmpty()) { + null + } else dateTimeFormatter.parseDateTime(json.asString) + } + } + private fun storeGradedCourses(exams: List) { val gradesStore = GradesStore(defaultSharedPreferences) val courses = exams.map { it.course } @@ -136,7 +248,11 @@ class GradesFragment : FragmentForAccessingTumOnline( PieEntry(count.toFloat(), grade) } - val set = PieDataSet(entries, getString(R.string.grades_without_weight)).apply { + var annotation = "" + if (!adaptDiagramToWeights) { + annotation = getString(R.string.grades_without_weight) + } + val set = PieDataSet(entries, annotation).apply { setColors(GRADE_COLORS, requireContext()) setDrawValues(false) } @@ -173,7 +289,11 @@ class GradesFragment : FragmentForAccessingTumOnline( BarEntry(index.toFloat(), value.toFloat()) } - val set = BarDataSet(entries, getString(R.string.grades_without_weight)).apply { + var annotation = "" + if (!adaptDiagramToWeights) { + annotation = getString(R.string.grades_without_weight) + } + val set = BarDataSet(entries, annotation).apply { setColors(GRADE_COLORS, requireContext()) valueTextColor = resources.getColor(R.color.text_primary) } @@ -201,16 +321,16 @@ class GradesFragment : FragmentForAccessingTumOnline( description = null setTouchEnabled(false) legend.setCustom( - arrayOf( - LegendEntry( - getString(R.string.grades_without_weight), - Legend.LegendForm.SQUARE, - 10f, - 0f, - null, - ContextCompat.getColor(context, R.color.grade_default) - ) + arrayOf( + LegendEntry( + getString(R.string.grades_without_weight), + Legend.LegendForm.SQUARE, + 10f, + 0f, + null, + ContextCompat.getColor(context, R.color.grade_default) ) + ) ) legend.textColor = getColor(resources, R.color.text_primary, null) @@ -232,11 +352,19 @@ class GradesFragment : FragmentForAccessingTumOnline( private fun calculateAverageGrade(exams: List): Double { val numberFormat = NumberFormat.getInstance(Locale.GERMAN) val grades = exams - .filter { it.isPassed } - .map { numberFormat.parse(it.grade).toDouble() } + .filter { it.isPassed && it.gradeUsedInAverage } + .map { + (numberFormat.parse(it.grade.toString())?.toDouble() + ?: 1.0) * it.credits_new * it.weight + } + var factorSum = exams + .filter { it.isPassed && it.gradeUsedInAverage } + .map { it.credits_new.toDouble() * it.weight }.sum() val gradeSum = grades.sum() - return gradeSum / grades.size.toDouble() + + factorSum = Math.max(factorSum, 0.0) + return gradeSum / factorSum } /** @@ -249,12 +377,19 @@ class GradesFragment : FragmentForAccessingTumOnline( val gradeDistribution = ArrayMap() exams.forEach { exam -> // The grade distribution now takes grades with more than one decimal place into account as well - var cleanGrade = exam.grade!! - if (cleanGrade.contains(longGradeRe)) { - cleanGrade = cleanGrade.subSequence(0, 3) as String + if (exam.gradeUsedInAverage) { + var cleanGrade = exam.grade!! + if (cleanGrade.contains(longGradeRe)) { + cleanGrade = cleanGrade.subSequence(0, 3) as String + } + val count = gradeDistribution[cleanGrade] ?: 0 + + if (adaptDiagramToWeights) { + gradeDistribution[cleanGrade] = count + (exam.credits_new * exam.weight).toInt() + } else { + gradeDistribution[cleanGrade] = count + 1 + } } - val count = gradeDistribution[cleanGrade] ?: 0 - gradeDistribution[cleanGrade] = count + 1 } return gradeDistribution } @@ -267,38 +402,162 @@ class GradesFragment : FragmentForAccessingTumOnline( */ private fun initSpinner(exams: List) { val programIds = exams - .map { it.programID } - .distinct() - .map { getString(R.string.study_program_format_string, it) } + .map { it.programID } + .distinct() + .map { getString(R.string.study_program_format_string, it) } + + if (programIds.size < 2) { + binding.filterSpinner.visibility = View.GONE + } else { + val filters = mutableListOf(getString(R.string.all_programs)) + filters.addAll(programIds) + + val spinnerArrayAdapter = ArrayAdapter( + requireContext(), R.layout.simple_spinner_item_actionbar, filters + ) + + with(binding) { + filterSpinner.apply { + adapter = spinnerArrayAdapter + setSelection(spinnerPosition) + visibility = View.VISIBLE + } + } - val filters = mutableListOf(getString(R.string.all_programs)) - filters.addAll(programIds) + binding.filterSpinner.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>, + view: View?, + position: Int, + id: Long + ) { + val filter = filters[position] + spinnerPosition = position + + val examsToShow = when (position) { + 0 -> exams + else -> exams.filter { filter.contains(it.programID) } + } + + showExams(examsToShow) + } - val spinnerArrayAdapter = ArrayAdapter( - requireContext(), R.layout.simple_spinner_item_actionbar, filters) + override fun onNothingSelected(parent: AdapterView<*>) = Unit + } + } + } - with(binding) { - filterSpinner.apply { - adapter = spinnerArrayAdapter - setSelection(spinnerPosition) - visibility = View.VISIBLE + /** + * Prompt the user to type in a custom exam grade. + */ + private fun openAddGradeDialog() { + val view = View.inflate(requireContext(), R.layout.dialog_add_grade_input, null) + val dialog = AlertDialog.Builder(requireContext()) + .setTitle(getString(R.string.add_exam_dialog_title)) + .setMessage( + getString(R.string.add_exam_dialog_message) + ) + .setView(view) + .create() + .apply { + window?.setBackgroundDrawableResource(R.drawable.rounded_corners_background) + } + + dialog.show() + dialog.findViewById