Skip to content
Closed
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
@@ -0,0 +1,55 @@
package org.openedx.core.domain.model

import org.openedx.core.BlockType

/**
* Assignment subsections with library_content wrappers may not roll up completion on the LMS
* blocks API. Treat complete when all descendant problems are done.
*/
fun Block.isAssignmentCompleted(allBlocks: List<Block>): Boolean {
if (isCompleted()) return true

val problems = collectDescendantBlocks(allBlocks).filter { it.type == BlockType.PROBLEM }
return problems.isNotEmpty() && problems.all { it.completion == 1.0 }
}

fun Block.assignmentCompletion(allBlocks: List<Block>): Double {

Check warning

Code scanning / detekt

Restrict the number of return statements in methods. Warning

Function assignmentCompletion has 3 return statements which exceeds the limit of 2.
if (isCompleted()) return 1.0

val problems = collectDescendantBlocks(allBlocks).filter { it.type == BlockType.PROBLEM }
if (problems.isEmpty()) return completion

return problems.count { it.completion == 1.0 }.toDouble() / problems.size
}

/**
* Prefer LMS course_progress (Sumac-style). Fall back to MCQ rollup when LMS total is missing.
*/
fun assignmentProgress(courseStructure: CourseStructure, assignments: List<Block>): Progress {
val lmsProgress = courseStructure.progress
return if (lmsProgress != null && lmsProgress.total > 0) {
lmsProgress
} else {
val blockData = courseStructure.blockData
Progress(
completed = assignments.count { it.isAssignmentCompleted(blockData) },
total = assignments.size,
)
}
}

private fun Block.collectDescendantBlocks(allBlocks: List<Block>): List<Block> {
if (descendants.isEmpty() || allBlocks.isEmpty()) return emptyList()

val blockMap = allBlocks.associateBy { it.id }
val result = mutableListOf<Block>()

fun walk(blockId: String) {
val block = blockMap[blockId] ?: return
result.add(block)
block.descendants.forEach { walk(it) }
}

descendants.forEach { walk(it) }
return result
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ sealed class CourseAssignmentUIState {
val groupedAssignments: Map<String, List<Block>>,
val courseProgress: CourseProgress,
val progress: Progress,
val sectionNames: Map<String, String>
val sectionNames: Map<String, String>,
val allBlocks: List<Block>,
) : CourseAssignmentUIState()
data object Empty : CourseAssignmentUIState()
data object Loading : CourseAssignmentUIState()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import kotlinx.coroutines.launch
import org.openedx.core.domain.model.Block
import org.openedx.core.domain.model.CourseProgress
import org.openedx.core.domain.model.CourseStructure
import org.openedx.core.domain.model.Progress
import org.openedx.core.domain.model.assignmentProgress
import org.openedx.core.system.notifier.CourseNotifier
import org.openedx.core.system.notifier.CourseStructureUpdated
import org.openedx.course.domain.interactor.CourseInteractor
Expand Down Expand Up @@ -79,16 +79,15 @@ class CourseAssignmentViewModel(
val grouped = filteredAssignments
.groupBy { it.assignmentProgress?.assignmentType ?: "" }
.toSortedMap(compareBy { assignmentTypeOrder.indexOf(it) })
val completed = assignments.count { it.isCompleted() }
val total = assignments.size
val progress = Progress(completed, total)
val progress = assignmentProgress(courseStructure, filteredAssignments)
val sectionName =
createAssignmentToChapterMapping(courseStructure.blockData, assignments)
createAssignmentToChapterMapping(courseStructure.blockData, filteredAssignments)
_uiState.value = CourseAssignmentUIState.CourseData(
groupedAssignments = grouped,
courseProgress = courseProgress,
progress = progress,
sectionNames = sectionName
sectionNames = sectionName,
allBlocks = courseStructure.blockData,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ import androidx.fragment.app.FragmentManager
import org.openedx.core.CoreMocks
import org.openedx.core.domain.model.Block
import org.openedx.core.domain.model.Progress
import org.openedx.core.domain.model.assignmentCompletion
import org.openedx.core.domain.model.isAssignmentCompleted
import org.openedx.core.ui.theme.OpenEdXTheme
import org.openedx.core.ui.theme.appColors
import org.openedx.core.ui.theme.appShapes
Expand Down Expand Up @@ -185,6 +187,7 @@ private fun CourseContentAssignmentScreen(
gradeColor = gradeColor,
assignments = blocks,
sectionNames = uiState.sectionNames,
allBlocks = uiState.allBlocks,
onAssignmentClick = onAssignmentClick,
)
}
Expand All @@ -202,18 +205,19 @@ private fun AssignmentGroupSection(
sectionNames: Map<String, String>,
percentOfGrade: Int,
gradeColor: Color,
allBlocks: List<Block>,
onAssignmentClick: (Block) -> Unit,
) {
val progress = Progress(
total = assignments.size,
completed = assignments.filter { it.isCompleted() }.size
completed = assignments.count { it.isAssignmentCompleted(allBlocks) }
)
val description = stringResource(
id = R.string.course_completed_of,
progress.completed,
progress.total
)
val firstUncompletedId = assignments.firstOrNull { !it.isCompleted() }?.id
val firstUncompletedId = assignments.firstOrNull { !it.isAssignmentCompleted(allBlocks) }?.id
var selectedId by rememberSaveable(label) { mutableStateOf(firstUncompletedId) }
var isCompletedShown by rememberSaveable { mutableStateOf(false) }

Expand Down Expand Up @@ -272,6 +276,7 @@ private fun AssignmentGroupSection(
items(assignments) { assignment ->
AssignmentButton(
assignment = assignment,
allBlocks = allBlocks,
isSelected = assignment.id == selectedId,
onClick = {
selectedId = assignment.id
Expand All @@ -287,6 +292,7 @@ private fun AssignmentGroupSection(
modifier = Modifier
.padding(horizontal = 24.dp),
assignment = assignment,
allBlocks = allBlocks,
sectionName = sectionNames[assignment.id] ?: "",
onAssignmentClick = onAssignmentClick
)
Expand All @@ -298,6 +304,7 @@ private fun AssignmentGroupSection(
.padding(horizontal = 24.dp)
.padding(top = 12.dp),
assignment = assignment,
allBlocks = allBlocks,
sectionName = sectionNames[assignment.id] ?: "",
onAssignmentClick = onAssignmentClick
)
Expand All @@ -311,21 +318,27 @@ private fun AssignmentGroupSection(
}

@Composable
private fun AssignmentButton(assignment: Block, isSelected: Boolean, onClick: () -> Unit) {
private fun AssignmentButton(
assignment: Block,
allBlocks: List<Block>,
isSelected: Boolean,
onClick: () -> Unit,
) {
val isDuePast = assignment.due != null && assignment.due!! < Date()
val isCompleted = assignment.isAssignmentCompleted(allBlocks)
val cardBorderColor = when {
isSelected -> MaterialTheme.appColors.primary
assignment.isCompleted() -> MaterialTheme.appColors.successGreen
isCompleted -> MaterialTheme.appColors.successGreen
isDuePast -> MaterialTheme.appColors.warning
else -> MaterialTheme.appColors.textDark
}
val icon = when {
assignment.isCompleted() -> painterResource(id = coreR.drawable.ic_core_check)
isCompleted -> painterResource(id = coreR.drawable.ic_core_check)
isDuePast -> painterResource(id = coreR.drawable.ic_core_watch_later)
else -> null
}
val iconDescription = when {
assignment.isCompleted() -> stringResource(R.string.course_accessibility_assignment_completed)
isCompleted -> stringResource(R.string.course_accessibility_assignment_completed)
isDuePast -> stringResource(R.string.course_accessibility_assignment_completed)
else -> null
}
Expand All @@ -334,7 +347,7 @@ private fun AssignmentButton(assignment: Block, isSelected: Boolean, onClick: ()
else -> 1.dp
}
val cardBackground = when {
assignment.isCompleted() -> MaterialTheme.appColors.successGreen.copy(
isCompleted -> MaterialTheme.appColors.successGreen.copy(
ASSIGNMENT_BUTTON_CARD_BACKGROUND_ALPHA
)

Expand Down Expand Up @@ -410,6 +423,7 @@ private fun AssignmentButton(assignment: Block, isSelected: Boolean, onClick: ()
private fun AssignmentDetails(
modifier: Modifier = Modifier,
assignment: Block,
allBlocks: List<Block>,
sectionName: String,
onAssignmentClick: (Block) -> Unit,
) {
Expand All @@ -418,15 +432,16 @@ private fun AssignmentDetails(
TimeUtils.formatToDueInString(LocalContext.current, it)
} ?: ""
val isDuePast = assignment.due != null && assignment.due!! < Date()
val progress = assignment.completion.toFloat()
val isCompleted = assignment.isAssignmentCompleted(allBlocks)
val progress = assignment.assignmentCompletion(allBlocks).toFloat()
val color = when {
assignment.isCompleted() -> MaterialTheme.appColors.successGreen
isCompleted -> MaterialTheme.appColors.successGreen
isDuePast -> MaterialTheme.appColors.warning
else -> MaterialTheme.appColors.assignmentCardBorder
}
val label = assignment.assignmentProgress?.label
val description = when {
assignment.isCompleted() -> {
isCompleted -> {
"$label " + stringResource(
R.string.course_complete_points,
assignment.assignmentProgress?.toPointString() ?: ""
Expand Down Expand Up @@ -529,7 +544,8 @@ private fun CourseContentAssignmentScreenPreview() {
"Homework" to listOf(CoreMocks.mockChapterBlock, CourseMocks.sequentialBlock)
),
courseProgress = CoreMocks.mockCourseProgress,
sectionNames = mapOf()
sectionNames = mapOf(),
allBlocks = CoreMocks.mockBlockData,
),
onAssignmentClick = {},
onNavigateToHome = {},
Expand Down Expand Up @@ -563,7 +579,8 @@ private fun CourseContentAssignmentScreenTabletPreview() {
"Quiz" to listOf(CourseMocks.sequentialBlock)
),
courseProgress = CoreMocks.mockCourseProgress,
sectionNames = mapOf()
sectionNames = mapOf(),
allBlocks = CoreMocks.mockBlockData,
),
onAssignmentClick = {},
onNavigateToHome = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import org.openedx.core.domain.model.Block
import org.openedx.core.domain.model.isAssignmentCompleted
import org.openedx.core.ui.theme.appColors
import org.openedx.core.ui.theme.appTypography
import org.openedx.core.utils.TimeUtils
Expand Down Expand Up @@ -64,9 +65,21 @@ fun AssignmentsHomePagerCardContent(
return
}

val completedAssignments = uiState.courseAssignments.count { it.isCompleted() }
val totalAssignments = uiState.courseAssignments.size
val firstIncompleteAssignment = uiState.courseAssignments.find { !it.isCompleted() }
val blockData = uiState.courseStructure.blockData
val lmsProgress = uiState.courseStructure.progress
val completedAssignments = if (lmsProgress != null && lmsProgress.total > 0) {
lmsProgress.completed
} else {
uiState.courseAssignments.count { it.isAssignmentCompleted(blockData) }
}
val totalAssignments = if (lmsProgress != null && lmsProgress.total > 0) {
lmsProgress.total
} else {
uiState.courseAssignments.size
}
val firstIncompleteAssignment = uiState.courseAssignments.find {
!it.isAssignmentCompleted(blockData)
}

Column(
modifier = Modifier
Expand Down
Loading