Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class SpeedGraderGradeInteractionTest : TeacherComposeTest() {

@Test
fun correctViewsForPointGradedWithoutRubric() {
goToSpeedGraderGradePage(gradingType = GradingType.points)
goToSpeedGraderGradePage(gradingType = GradingType.points, score = 10.0, grade = "10")

speedGraderGradePage.assertSpeedGraderLabelDisplayed()
speedGraderGradePage.assertCurrentEnteredScore("10")
Expand Down Expand Up @@ -156,14 +156,14 @@ class SpeedGraderGradeInteractionTest : TeacherComposeTest() {

speedGraderGradePage.assertFinalGradePointsValueDisplayed("12 / 20 pts")
speedGraderGradePage.assertLatePenaltyValueDisplayed("0 pts")
speedGraderGradePage.assertFinalGradeIsDisplayed("12.0")
speedGraderGradePage.assertFinalGradeIsDisplayed("60%")

speedGraderGradePage.assertNoRubricCriterionDisplayed()
}

@Test
fun correctViewsForPassFailAssignment() {
goToSpeedGraderGradePage(gradingType = GradingType.pass_fail)
goToSpeedGraderGradePage(gradingType = GradingType.pass_fail, score = 10.0)

speedGraderGradePage.assertSpeedGraderLabelDisplayed()
speedGraderGradePage.assertCurrentEnteredPassFailScore("10 / 20")
Expand Down Expand Up @@ -196,7 +196,7 @@ class SpeedGraderGradeInteractionTest : TeacherComposeTest() {

@Test
fun correctViewsForGpaScaleAssignment() {
goToSpeedGraderGradePage(GradingType.gpa_scale)
goToSpeedGraderGradePage(GradingType.gpa_scale, score = 10.0)
speedGraderGradePage.assertSpeedGraderLabelDisplayed()
speedGraderGradePage.assertCurrentEnteredScore("10")
speedGraderGradePage.assertPointsPossible("20")
Expand All @@ -217,7 +217,7 @@ class SpeedGraderGradeInteractionTest : TeacherComposeTest() {

@Test
fun correctViewsForLetterGradeAssignment() {
goToSpeedGraderGradePage(gradingType = GradingType.letter_grade)
goToSpeedGraderGradePage(gradingType = GradingType.letter_grade, score = 10.0)

speedGraderGradePage.assertSpeedGraderLabelDisplayed()
speedGraderGradePage.assertCurrentEnteredScore("10")
Expand Down Expand Up @@ -320,6 +320,8 @@ class SpeedGraderGradeInteractionTest : TeacherComposeTest() {
gradingType: GradingType = GradingType.points,
hasRubric: Boolean = false,
pointsPossible: Int = 20,
score: Double = 12.0,
grade: String = "60%",
submission: Submission? = null
) {
val data = MockCanvas.init(teacherCount = 1, courseCount = 1, favoriteCourseCount = 1, studentCount = 1)
Expand Down Expand Up @@ -366,8 +368,8 @@ class SpeedGraderGradeInteractionTest : TeacherComposeTest() {
userId = student.id,
type = "online_text_entry",
body = "This is a test submission",
score = 10.0,
grade = "60"
score = score,
grade = grade
)

val token = data.tokenFor(teacher)!!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,8 +364,15 @@ private fun LateHeader(
submissionDate: Date?,
onLateDaysChange: (Float?) -> Unit
) {
var textFieldValue by remember(daysLate) {
mutableStateOf(numberFormatter.format(daysLate),)
var textFieldValue by remember {
mutableStateOf(numberFormatter.format(daysLate))
}

LaunchedEffect(daysLate) {
val apiFormatted = numberFormatter.format(daysLate)
if (textFieldValue != apiFormatted) {
textFieldValue = apiFormatted
}
}

Row(
Expand Down Expand Up @@ -424,28 +431,37 @@ private fun LetterGradeGradingTypeInput(uiState: SpeedGraderGradingUiState) {
val defaultItem =
if (uiState.excused) stringResource(R.string.gradeExcused) else stringResource(R.string.not_graded)

var textFieldScore by remember(uiState.enteredScore) {
mutableStateOf(uiState.enteredScore?.let {
numberFormatter.format(
it
)
}.orEmpty())
val initialScore = uiState.enteredScore
val initialGrade = initialScore?.let {
convertScoreToLetterGrade(
it.toDouble(),
uiState.pointsPossible.orDefault(),
uiState.letterGrades
)
} ?: defaultItem

var textFieldScore by remember {
mutableStateOf(initialScore?.let { numberFormatter.format(it) }.orEmpty())
}

var selectedGrade by remember(
uiState.enteredScore,
uiState.pointsPossible,
uiState.letterGrades
) {
mutableStateOf(
uiState.enteredScore?.let {
var selectedGrade by remember {
mutableStateOf(initialGrade)
}

LaunchedEffect(uiState.enteredScore) {
val apiScore = uiState.enteredScore
val apiFormatted = apiScore?.let { numberFormatter.format(it) }.orEmpty()

if (textFieldScore != apiFormatted) {
textFieldScore = apiFormatted
selectedGrade = apiScore?.let {
convertScoreToLetterGrade(
it.toDouble(),
uiState.pointsPossible.orDefault(),
uiState.letterGrades
)
} ?: defaultItem
)
}
}

LaunchedEffect(textFieldScore) {
Expand All @@ -456,7 +472,7 @@ private fun LetterGradeGradingTypeInput(uiState: SpeedGraderGradingUiState) {
}

LaunchedEffect(selectedGrade) {
if (selectedGrade != uiState.enteredGrade && uiState.letterGrades.any { it.name == selectedGrade }) {
if (selectedGrade != defaultItem && selectedGrade != uiState.enteredGrade && uiState.letterGrades.any { it.name == selectedGrade }) {
uiState.onPercentageChange(
uiState.letterGrades
.find { it.name == selectedGrade }
Expand Down Expand Up @@ -526,9 +542,17 @@ private fun LetterGradeGradingTypeInput(uiState: SpeedGraderGradingUiState) {
@Composable
private fun CompleteIncompleteGradingTypeInput(uiState: SpeedGraderGradingUiState) {
val haptic = LocalHapticFeedback.current
var grade by remember(uiState.enteredGrade) {
var grade by remember {
mutableStateOf(uiState.enteredGrade.orEmpty())
}

LaunchedEffect(uiState.enteredGrade) {
val apiGrade = uiState.enteredGrade.orEmpty()
if (grade != apiGrade) {
grade = apiGrade
}
}

Column {
Row(
modifier = Modifier
Expand Down Expand Up @@ -587,48 +611,58 @@ private fun CompleteIncompleteGradingTypeInput(uiState: SpeedGraderGradingUiStat
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PercentageGradingTypeInput(uiState: SpeedGraderGradingUiState) {
val grade = uiState.enteredGrade?.replace("%", "").orEmpty()
var sliderDrivenScore by remember { mutableFloatStateOf(grade.toFloatOrNull() ?: 0f) }
var textFieldScore by remember(uiState.enteredGrade) { mutableStateOf(grade) }

val maxScore by remember(uiState.enteredGrade) {
mutableFloatStateOf(
max(
grade.toFloatOrNull() ?: 0f, 100f
)
)
val initialGrade = uiState.enteredGrade?.replace("%", "").orEmpty()
val initialGradeAsFloat = initialGrade.toFloatOrNull() ?: 0f

var sliderDrivenScore by remember {
mutableFloatStateOf(initialGradeAsFloat)
}
var textFieldScore by remember {
mutableStateOf(if (initialGradeAsFloat == 0f) "" else numberFormatter.format(initialGradeAsFloat))
}

var maxScore by remember {
mutableFloatStateOf(max(initialGradeAsFloat, 100f))
}

val sliderState = remember(maxScore) {
SliderState(
value = sliderDrivenScore.coerceAtLeast(0f),
value = sliderDrivenScore.coerceIn(0f, maxScore),
valueRange = 0f..maxScore,
)
}

LaunchedEffect(textFieldScore) {
val scoreAsFloat = textFieldScore.toFloatOrNull()
if (scoreAsFloat != uiState.enteredScore) {
uiState.onPercentageChange(scoreAsFloat)
LaunchedEffect(uiState.enteredGrade) {
val apiGrade = uiState.enteredGrade?.replace("%", "")?.toFloatOrNull()
val apiFormatted = apiGrade?.let { if (it == 0f) "" else numberFormatter.format(it) }.orEmpty()

maxScore = max(apiGrade ?: 0f, 100f)

if (textFieldScore != apiFormatted) {
textFieldScore = apiFormatted
val newValue = apiGrade ?: 0f
sliderDrivenScore = newValue
}
}

LaunchedEffect(grade) {
val newScore = grade.toFloatOrNull()
if (textFieldScore != newScore?.toString()) {
textFieldScore = newScore?.let { numberFormatter.format(it) }.orEmpty()
LaunchedEffect(textFieldScore) {
val scoreAsFloat = textFieldScore.toFloatOrNull() ?: 0f
if (sliderDrivenScore != scoreAsFloat) {
sliderDrivenScore = scoreAsFloat
sliderState.value = scoreAsFloat.coerceIn(0f, maxScore)
maxScore = max(scoreAsFloat, maxScore)
}
if (sliderDrivenScore != (newScore ?: 0f)) {
sliderDrivenScore = newScore ?: 0f
sliderState.value = (newScore ?: 0f).coerceAtLeast(0f)
val currentPercentage = uiState.enteredGrade?.replace("%", "")?.toFloatOrNull()
if (scoreAsFloat != currentPercentage) {
uiState.onPercentageChange(textFieldScore.toFloatOrNull())
}
}

LaunchedEffect(sliderState.value) {
val newScoreFromSlider = sliderState.value
val newScoreFromSlider = round(sliderState.value)
if (sliderDrivenScore != newScoreFromSlider) {
sliderDrivenScore = round(newScoreFromSlider)
uiState.onPercentageChange(sliderDrivenScore)
textFieldScore = numberFormatter.format(sliderDrivenScore)
sliderDrivenScore = newScoreFromSlider
textFieldScore = numberFormatter.format(newScoreFromSlider)
}
}

Expand Down Expand Up @@ -693,80 +727,81 @@ private fun PercentageGradingTypeInput(uiState: SpeedGraderGradingUiState) {
private fun PointGradingTypeInput(uiState: SpeedGraderGradingUiState) {
val haptic = LocalHapticFeedback.current

val maxScore by remember(uiState.enteredScore) {
mutableFloatStateOf(
max(
(uiState.pointsPossible?.toFloat() ?: 10f),
uiState.enteredScore ?: 0f
)
)
val initialScore = uiState.enteredScore ?: 0f
val naturalMaxScore = uiState.pointsPossible?.toFloat() ?: 10f
val initialMaxScore = max(naturalMaxScore, initialScore)
val initialMinScore = min(initialScore, 0f)
val initialPointScale = when {
initialMaxScore <= 10.0 -> 4f
initialMaxScore <= 20.0 -> 2f
else -> 1f
}

val pointScale by remember(maxScore) {
mutableFloatStateOf(
when {
maxScore <= 10.0 -> 4f
maxScore <= 20.0 -> 2f
else -> 1f
}
)
var maxScore by remember {
mutableFloatStateOf(initialMaxScore)
}

var sliderDrivenScore by remember(uiState.enteredScore) {
mutableFloatStateOf(
(uiState.enteredScore ?: 0f) * pointScale
)
var minScore by remember {
mutableFloatStateOf(initialMinScore)
}
var textFieldScore by remember(uiState.enteredScore) {
mutableStateOf(uiState.enteredScore?.let {
numberFormatter.format(
it
)
}.orEmpty())

val pointScale by remember {
mutableFloatStateOf(initialPointScale)
}

val minScore by remember(uiState.enteredScore) {
mutableFloatStateOf(
min(
uiState.enteredScore ?: 0f,
0f
)
var sliderDrivenScore by remember {
mutableFloatStateOf(initialScore * initialPointScale)
}
var textFieldScore by remember {
mutableStateOf(
if (initialScore == 0f) "" else numberFormatter.format(initialScore)
)
}

val sliderState = remember(uiState.enteredScore, maxScore, minScore) {
val sliderState = remember(maxScore, minScore) {
SliderState(
value = sliderDrivenScore,
value = sliderDrivenScore.coerceIn(minScore * pointScale, maxScore * pointScale),
valueRange = minScore * pointScale..maxScore * pointScale,
steps = ((maxScore - minScore).roundToInt() * pointScale.roundToInt() - 1).coerceAtLeast(
1
)
steps = ((maxScore - minScore).roundToInt() * pointScale.roundToInt() - 1).coerceAtLeast(1)
)
}

LaunchedEffect(textFieldScore) {
val scoreAsFloat = textFieldScore.toFloatOrNull()
if (scoreAsFloat != uiState.enteredScore) {
uiState.onScoreChange(scoreAsFloat)
LaunchedEffect(uiState.enteredScore) {
val apiScore = uiState.enteredScore
val apiFormatted = apiScore?.let { numberFormatter.format(it) }.orEmpty()

maxScore = max(apiScore ?: 0f, naturalMaxScore)
minScore = min(apiScore ?: 0f, 0f)

if (textFieldScore != apiFormatted) {
textFieldScore = apiFormatted
val newValue = (apiScore ?: 0f) * pointScale
sliderDrivenScore = newValue
sliderState.value = newValue
}
}

LaunchedEffect(uiState.enteredScore) {
val newScore = uiState.enteredScore
if (textFieldScore != newScore?.toString()) {
textFieldScore = newScore?.let { numberFormatter.format(it) }.orEmpty()
LaunchedEffect(textFieldScore) {
val scoreAsFloat = textFieldScore.toFloatOrNull()
val scaledScore = (scoreAsFloat ?: 0f) * pointScale

if (sliderDrivenScore != scaledScore) {
sliderDrivenScore = scaledScore
maxScore = max(scoreAsFloat ?: 0f, maxScore)
minScore = min(scoreAsFloat ?: 0f, minScore)
sliderState.value = scaledScore.coerceIn(minScore * pointScale, maxScore * pointScale)
}
if (sliderDrivenScore != (newScore ?: 0f)) {
sliderDrivenScore = (newScore ?: 0f) * pointScale
sliderState.value = sliderDrivenScore

if (scoreAsFloat != uiState.enteredScore) {
uiState.onScoreChange(scoreAsFloat)
}
}

LaunchedEffect(sliderState.value) {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
val newScoreFromSlider = sliderState.value.roundToInt().toFloat() / pointScale
if (sliderDrivenScore != newScoreFromSlider) {
sliderDrivenScore = newScoreFromSlider
if (sliderDrivenScore != sliderState.value) {
sliderDrivenScore = sliderState.value
textFieldScore = numberFormatter.format(newScoreFromSlider)
}
}
Expand Down
Loading