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
2 changes: 1 addition & 1 deletion core/src/main/java/org/openedx/core/data/api/CourseApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
@GET(
"/api/mobile/{api_version}/course_info/blocks/?" +
"depth=all&" +
"requested_fields=contains_gated_content,show_gated_sections,special_exam_info,graded,format," +
"requested_fields=contains_gated_content,show_gated_sections,gated_content,special_exam_info,graded,format," +

Check warning

Code scanning / detekt

Reports lines with exceeded length Warning

Exceeded max line length (120)

Check warning

Code scanning / detekt

Line detected, which is longer than the defined maximum line length in the code style. Warning

Line detected, which is longer than the defined maximum line length in the code style.
"student_view_multi_device,due,completion&" +
"student_view_data=video,discussion&" +
"block_counts=video&" +
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/java/org/openedx/core/data/model/Block.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ data class Block(
val completion: Double?,
@SerializedName("contains_gated_content")
val containsGatedContent: Boolean?,
@SerializedName("gated_content")
val gatedContent: GatedContent?,
@SerializedName("assignment_progress")
val assignmentProgress: AssignmentProgress?,
@SerializedName("due")
Expand All @@ -63,6 +65,7 @@ data class Block(
studentViewData = studentViewData?.mapToDomain(),
studentViewMultiDevice = studentViewMultiDevice ?: false,
blockCounts = blockCounts?.mapToDomain()!!,
gatedContent = gatedContent?.mapToDomain(),
completion = completion ?: 0.0,
containsGatedContent = containsGatedContent ?: false,
assignmentProgress = assignmentProgress?.mapToDomain(),
Expand Down
35 changes: 35 additions & 0 deletions core/src/main/java/org/openedx/core/data/model/GatedContent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.openedx.core.data.model

import com.google.gson.annotations.SerializedName
import org.openedx.core.data.model.room.GatedContentDb
import org.openedx.core.domain.model.GatedContent as DomainGatedContent

data class GatedContent(
@SerializedName("prereq_id")
val prereqId: String?,
@SerializedName("prereq_section_name")
val prereqSectionName: String?,
@SerializedName("gated")
val gated: Boolean?,
@SerializedName("gated_section_name")
val gatedSectionName: String?,
@SerializedName("prereq_url")
val prereqUrl: String?
) {
fun mapToDomain() = DomainGatedContent(
prereqId = prereqId.orEmpty(),
prereqSectionName = prereqSectionName.orEmpty(),
gated = gated ?: false,
gatedSectionName = gatedSectionName.orEmpty(),
prereqUrl = prereqUrl.orEmpty()
)

fun mapToRoomEntity() = GatedContentDb(
prereqId = prereqId,
prereqSectionName = prereqSectionName,
gated = gated,
gatedSectionName = gatedSectionName,
prereqUrl = prereqUrl
)
}

25 changes: 25 additions & 0 deletions core/src/main/java/org/openedx/core/data/model/room/BlockDb.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ data class BlockDb(
@ColumnInfo("contains_gated_content")
val containsGatedContent: Boolean,
@Embedded
val gatedContent: GatedContentDb?,
@Embedded
val assignmentProgress: AssignmentProgressDb?,
@ColumnInfo("due")
val due: String?,
Expand Down Expand Up @@ -78,6 +80,7 @@ data class BlockDb(
blockCounts = blockCounts.mapToDomain(),
descendants = descendants,
descendantsType = descendantsType,
gatedContent = gatedContent?.mapToDomain(),
completion = completion,
containsGatedContent = containsGatedContent,
assignmentProgress = assignmentProgress?.mapToDomain(),
Expand All @@ -104,6 +107,7 @@ data class BlockDb(
graded = graded ?: false,
studentViewData = StudentViewDataDb.createFrom(studentViewData),
studentViewMultiDevice = studentViewMultiDevice ?: false,
gatedContent = gatedContent?.mapToRoomEntity(),
blockCounts = BlockCountsDb.createFrom(blockCounts),
completion = completion ?: 0.0,
containsGatedContent = containsGatedContent ?: false,
Expand Down Expand Up @@ -254,3 +258,24 @@ data class OfflineDownloadDb(
)
}
}

data class GatedContentDb(
@ColumnInfo("prereq_id")
val prereqId: String?,
@ColumnInfo("prereq_section_name")
val prereqSectionName: String?,
@ColumnInfo("gated")
val gated: Boolean?,
@ColumnInfo("gated_section_name")
val gatedSectionName: String?,
@ColumnInfo("prereq_url")
val prereqUrl: String?
) {
fun mapToDomain() = org.openedx.core.domain.model.GatedContent(
prereqId = prereqId.orEmpty(),
prereqSectionName = prereqSectionName.orEmpty(),
gated = gated ?: false,
gatedSectionName = gatedSectionName.orEmpty(),
prereqUrl = prereqUrl.orEmpty()
)
}
3 changes: 2 additions & 1 deletion core/src/main/java/org/openedx/core/domain/model/Block.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ data class Block(
val descendantsType: BlockType,
val completion: Double,
val containsGatedContent: Boolean = false,
val gatedContent: GatedContent? = null,
val downloadModel: DownloadModel? = null,
val assignmentProgress: AssignmentProgress?,
val due: Date?,
Expand Down Expand Up @@ -58,7 +59,7 @@ data class Block(

fun isDownloaded() = downloadModel?.downloadedState == DownloadedState.DOWNLOADED

fun isGated() = containsGatedContent
fun isGated() = containsGatedContent || gatedContent?.gated == true

fun isCompleted() = completion == 1.0

Expand Down
14 changes: 14 additions & 0 deletions core/src/main/java/org/openedx/core/domain/model/GatedContent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.openedx.core.domain.model

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
data class GatedContent(
val prereqId: String,
val prereqSectionName: String,
val gated: Boolean,
val gatedSectionName: String,
val prereqUrl: String
) : Parcelable

Original file line number Diff line number Diff line change
Expand Up @@ -240,12 +240,16 @@ class CourseOutlineViewModel(
val courseSectionsState =
(_uiState.value as? CourseOutlineUIState.CourseData)?.courseSectionsState.orEmpty()

// Create immutable copy of courseSubSections so Compose detects changes
// This is important for updating lock icons when prerequisite status changes
val courseSubSectionsCopy = courseSubSections.toMap()

_uiState.value = CourseOutlineUIState.CourseData(
courseStructure = sortedStructure,
downloadedState = getDownloadModelsStatus(),
resumeComponent = getResumeBlock(blocks, courseStatus.lastVisitedBlockId),
resumeUnitTitle = resumeVerticalBlock?.displayName ?: "",
courseSubSections = courseSubSections,
courseSubSections = courseSubSectionsCopy,
courseSectionsState = courseSectionsState,
subSectionsDownloadsCount = subSectionsDownloadsCount,
datesBannerInfo = datesBannerInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,15 +288,27 @@
val completedIconPainter =
if (block.isCompleted()) {
painterResource(R.drawable.course_ic_task_alt)
}
else if (block.isGated()) {

Check warning

Code scanning / detekt

Reports spaces around keywords Warning

Unexpected newline before "else"
painterResource(R.drawable.ic_lock)
} else {
painterResource(
CoreR.drawable.ic_core_chapter_icon
)
}
val completedIconColor =
if (block.isCompleted()) MaterialTheme.appColors.primary else MaterialTheme.appColors.onSurface
if (block.isCompleted()) {
MaterialTheme.appColors.primary
}
else if (block.isGated()) {

Check warning

Code scanning / detekt

Reports spaces around keywords Warning

Unexpected newline before "else"
MaterialTheme.appColors.textSecondary
} else {
MaterialTheme.appColors.onSurface
}
val completedIconDescription = if (block.isCompleted()) {
stringResource(id = R.string.course_accessibility_section_completed)
} else if (block.isGated()) {
stringResource(id = R.string.course_accessibility_section_locked)
} else {
stringResource(id = R.string.course_accessibility_section_uncompleted)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,16 +136,22 @@ fun CourseSectionCard(
) {
val completedIconPainter = if (block.isCompleted()) {
painterResource(R.drawable.course_ic_task_alt)
} else if (block.isGated()) {
painterResource(R.drawable.ic_lock)
} else {
painterResource(coreR.drawable.ic_core_chapter_icon)
}
val completedIconColor = if (block.isCompleted()) {
MaterialTheme.appColors.primary
} else if (block.isGated()) {
MaterialTheme.appColors.textSecondary
} else {
MaterialTheme.appColors.onSurface
}
val completedIconDescription = if (block.isCompleted()) {
stringResource(id = R.string.course_accessibility_section_completed)
} else if (block.isGated()) {
stringResource(id = R.string.course_accessibility_section_locked)
} else {
stringResource(id = R.string.course_accessibility_section_uncompleted)
}
Expand Down Expand Up @@ -780,11 +786,15 @@ fun CourseSubSectionItem(
val context = LocalContext.current
val icon = if (block.isCompleted()) {
painterResource(R.drawable.course_ic_task_alt)
} else if (block.isGated()) {
painterResource(R.drawable.ic_lock)
} else {
painterResource(coreR.drawable.ic_core_chapter_icon)
}
val iconColor = if (block.isCompleted()) {
MaterialTheme.appColors.successGreen
} else if (block.isGated()) {
MaterialTheme.appColors.textSecondary
} else {
MaterialTheme.appColors.onSurface
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package org.openedx.course.presentation.unit

import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.foundation.layout.*

Check warning

Code scanning / detekt

Detects wildcard imports Warning

Wildcard import

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

androidx.compose.foundation.layout.* is a wildcard import. Replace it with fully qualified imports.
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*

Check warning

Code scanning / detekt

Detects wildcard imports Warning

Wildcard import

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

androidx.compose.material.* is a wildcard import. Replace it with fully qualified imports.
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import org.koin.android.ext.android.inject
import org.openedx.core.presentation.course.CourseViewMode
import org.openedx.core.ui.OpenEdXButton
import org.openedx.core.ui.theme.OpenEdXTheme
import org.openedx.core.ui.theme.appColors
import org.openedx.core.ui.theme.appTypography
import org.openedx.course.presentation.CourseRouter
import org.openedx.foundation.extension.parcelable
import org.openedx.course.R as courseR

class PrerequisiteLockedFragment : Fragment() {

private val router by inject<CourseRouter>()

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
OpenEdXTheme {
val prereqData = arguments?.parcelable<PrerequisiteData>(ARG_PREREQUISITE_DATA)

PrerequisiteLockedScreen(
prereqSectionName = prereqData?.prereqSectionName ?: "",
onNavigateToPrerequisite = {
prereqData?.let { data ->
router.navigateToCourseSubsections(
fm = requireActivity().supportFragmentManager,
courseId = data.courseId,
subSectionId = data.prereqId,
unitId = "",
componentId = "",
mode = data.mode
)
}
}
)
}
}
}

companion object {
private const val ARG_PREREQUISITE_DATA = "prerequisite_data"

fun newInstance(
courseId: String,
prereqId: String,
prereqSectionName: String,
mode: CourseViewMode
): PrerequisiteLockedFragment {
val fragment = PrerequisiteLockedFragment()
fragment.arguments = Bundle().apply {
putParcelable(
ARG_PREREQUISITE_DATA,
PrerequisiteData(courseId, prereqId, prereqSectionName, mode)
)
}
return fragment
}
}
}

@kotlinx.parcelize.Parcelize
data class PrerequisiteData(
val courseId: String,
val prereqId: String,
val prereqSectionName: String,
val mode: CourseViewMode
) : android.os.Parcelable

@Composable
private fun PrerequisiteLockedScreen(
prereqSectionName: String,
onNavigateToPrerequisite: () -> Unit
) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.appColors.background
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp, vertical = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
painter = painterResource(id = courseR.drawable.ic_lock),
contentDescription = null,
tint = MaterialTheme.appColors.textPrimary,
modifier = Modifier.size(100.dp)
)

Spacer(modifier = Modifier.height(24.dp))

Text(
text = stringResource(id = courseR.string.course_content_locked),
style = MaterialTheme.appTypography.titleLarge,
color = MaterialTheme.appColors.textPrimary,
textAlign = TextAlign.Center
)

Spacer(modifier = Modifier.height(16.dp))

Text(
text = stringResource(
id = courseR.string.course_must_complete_prerequisite,
prereqSectionName
),
style = MaterialTheme.appTypography.bodyMedium,
color = MaterialTheme.appColors.textPrimary,
textAlign = TextAlign.Center
)

Spacer(modifier = Modifier.height(32.dp))

OpenEdXButton(
text = stringResource(id = courseR.string.course_go_to_prerequisite_section),
onClick = onNavigateToPrerequisite,
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
)
}
}
}

@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
private fun PrerequisiteLockedScreenPreview() {
OpenEdXTheme {
PrerequisiteLockedScreen(
prereqSectionName = "Introduction to Programming",
onNavigateToPrerequisite = {}
)
}
}
Loading
Loading