From 7055c5bbb690927f899abf4e92df9adde0d2a1f4 Mon Sep 17 00:00:00 2001 From: Yogesh Bhagat Date: Fri, 21 Nov 2025 13:32:51 +0530 Subject: [PATCH 1/4] add new fields for course extended details --- .../discovery/data/model/CourseDetails.kt | 12 ++++++++++++ .../openedx/discovery/data/model/Instructor.kt | 17 +++++++++++++++++ .../discovery/data/model/room/CourseEntity.kt | 16 ++++++++++++++++ .../openedx/discovery/domain/model/Course.kt | 6 +++++- .../presentation/search/CourseSearchFragment.kt | 6 +++++- .../discovery/presentation/ui/DiscoveryUI.kt | 6 +++++- 6 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 discovery/src/main/java/org/openedx/discovery/data/model/Instructor.kt diff --git a/discovery/src/main/java/org/openedx/discovery/data/model/CourseDetails.kt b/discovery/src/main/java/org/openedx/discovery/data/model/CourseDetails.kt index 4e9dd4314..f37dadf45 100644 --- a/discovery/src/main/java/org/openedx/discovery/data/model/CourseDetails.kt +++ b/discovery/src/main/java/org/openedx/discovery/data/model/CourseDetails.kt @@ -50,6 +50,14 @@ data class CourseDetails( val isEnrolled: Boolean?, @SerializedName("duration") val duration: String?, + @SerializedName("course_requirement") + val courseRequirement: String?, + @SerializedName("description") + val description: String?, + @SerializedName("learning_outcomes") + val learningOutcomes: List?, + @SerializedName("instructors") + val instructors: List?, ) { fun mapToDomain(): Course { @@ -76,6 +84,10 @@ data class CourseDetails( isEnrolled = isEnrolled ?: false, media = mapMediaToDomain(), duration = duration.orEmpty(), + courseRequirement = courseRequirement.orEmpty(), + description = description.orEmpty(), + learningOutcomes = learningOutcomes?.joinToString(", ") ?: "", + instructorsList = instructors ?: emptyList() ) } diff --git a/discovery/src/main/java/org/openedx/discovery/data/model/Instructor.kt b/discovery/src/main/java/org/openedx/discovery/data/model/Instructor.kt new file mode 100644 index 000000000..dc0acecbe --- /dev/null +++ b/discovery/src/main/java/org/openedx/discovery/data/model/Instructor.kt @@ -0,0 +1,17 @@ +package org.openedx.discovery.data.model + +import com.google.gson.annotations.SerializedName + +data class Instructor( + @SerializedName("name") + val name: String?, + @SerializedName("title") + val title: String?, + @SerializedName("organization") + val organization: String?, + @SerializedName("bio") + val bio: String?, + @SerializedName("image") + val image: String? +) + diff --git a/discovery/src/main/java/org/openedx/discovery/data/model/room/CourseEntity.kt b/discovery/src/main/java/org/openedx/discovery/data/model/room/CourseEntity.kt index 87f6e2a68..438188f70 100644 --- a/discovery/src/main/java/org/openedx/discovery/data/model/room/CourseEntity.kt +++ b/discovery/src/main/java/org/openedx/discovery/data/model/room/CourseEntity.kt @@ -56,6 +56,14 @@ data class CourseEntity( val isEnrolled: Boolean, @ColumnInfo("duration") val duration: String, + @ColumnInfo("courseRequirement") + val courseRequirement: String, + @ColumnInfo("description") + val description: String, + @ColumnInfo("learningOutcomes") + val learningOutcomes: String, + @ColumnInfo("instructors") + val instructors: String, ) { fun mapToDomain(): Course { @@ -82,6 +90,10 @@ data class CourseEntity( overview = overview, isEnrolled = isEnrolled, duration = duration, + courseRequirement = courseRequirement, + description = description, + learningOutcomes = learningOutcomes, + instructorsList = emptyList() ) } @@ -110,6 +122,10 @@ data class CourseEntity( media = MediaDb.createFrom(model.media), isEnrolled = model.isEnrolled ?: false, duration = model.duration.orEmpty(), + courseRequirement = model.courseRequirement.orEmpty(), + description = model.description.orEmpty(), + learningOutcomes = model.learningOutcomes?.joinToString(", ") ?: "", + instructors = model.instructors?.mapNotNull { it.name }?.joinToString(", ") ?: "", ) } } diff --git a/discovery/src/main/java/org/openedx/discovery/domain/model/Course.kt b/discovery/src/main/java/org/openedx/discovery/domain/model/Course.kt index 0500ed586..aa56cd966 100644 --- a/discovery/src/main/java/org/openedx/discovery/domain/model/Course.kt +++ b/discovery/src/main/java/org/openedx/discovery/domain/model/Course.kt @@ -25,5 +25,9 @@ data class Course( val startType: String, val overview: String, val isEnrolled: Boolean, - val duration: String + val duration: String, + val courseRequirement: String, + val description: String, + val learningOutcomes: String, + val instructorsList: List = emptyList() ) diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchFragment.kt index 6ac3d24cb..f5357f184 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchFragment.kt @@ -520,5 +520,9 @@ private val mockCourse = Course( startType = "startType", overview = "", isEnrolled = false, - duration = "30 Days" + duration = "30 Days", + courseRequirement = "", + description = "", + learningOutcomes = "", + instructorsList = emptyList() ) diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/ui/DiscoveryUI.kt b/discovery/src/main/java/org/openedx/discovery/presentation/ui/DiscoveryUI.kt index bf9eaac24..61293d82e 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/ui/DiscoveryUI.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/ui/DiscoveryUI.kt @@ -842,7 +842,11 @@ private val mockCourse = Course( startType = "startType", overview = "", isEnrolled = false, - duration = "30 Days" + duration = "30 Days", + courseRequirement = "", + description = "", + learningOutcomes = "", + instructorsList = emptyList() ) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) From 29e05d75c1864f37a99561328ab8406224366405 Mon Sep 17 00:00:00 2001 From: Yogesh Bhagat Date: Fri, 21 Nov 2025 13:36:39 +0530 Subject: [PATCH 2/4] add extended course details fields in course about screen --- .../detail/CourseDetailsFragment.kt | 381 +++++++++++++++--- discovery/src/main/res/values/strings.xml | 8 + 2 files changed, 331 insertions(+), 58 deletions(-) diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsFragment.kt index 151260d97..f27b6f858 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsFragment.kt @@ -14,6 +14,7 @@ import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -32,6 +33,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon @@ -41,6 +44,14 @@ import androidx.compose.material.Scaffold import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Assignment +import androidx.compose.material.icons.automirrored.filled.Rule +import androidx.compose.material.icons.filled.Description +import androidx.compose.material.icons.filled.Domain +import androidx.compose.material.icons.filled.Group +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.TrackChanges import androidx.compose.material.icons.outlined.Report import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable @@ -56,7 +67,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalConfiguration @@ -68,15 +81,19 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.zIndex import androidx.core.os.bundleOf import androidx.fragment.app.Fragment +import coil.compose.AsyncImage +import coil.request.ImageRequest import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -246,7 +263,7 @@ internal fun CourseDetailsScreen( } } } - ) { + ) { paddingValues -> val screenWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -260,21 +277,12 @@ internal fun CourseDetailsScreen( ) } - val webViewPadding by remember(key1 = windowSize) { - mutableStateOf( - windowSize.windowSizeValue( - expanded = Modifier.padding(vertical = 24.dp), - compact = Modifier.padding(horizontal = 16.dp, vertical = 24.dp) - ) - ) - } - HandleUIMessage(uiMessage = uiMessage, scaffoldState = scaffoldState) Box( modifier = Modifier .fillMaxSize() - .padding(it) + .padding(paddingValues) .statusBarsInset() .displayCutoutForLandscape(), contentAlignment = Alignment.TopCenter @@ -301,8 +309,7 @@ internal fun CourseDetailsScreen( is CourseDetailsUIState.Loading -> { Box( modifier = Modifier - .fillMaxSize() - .padding(it), + .fillMaxSize(), contentAlignment = Alignment.Center ) { CircularProgressIndicator(color = MaterialTheme.appColors.primary) @@ -334,42 +341,115 @@ internal fun CourseDetailsScreen( } ) } - if (isPreview) { - Text( - text = htmlBody, - modifier = Modifier - .testTag("txt_course_description") - .padding(all = 20.dp), + + // About this Course (with short description) + if (uiState.course.shortDescription.isNotBlank()) { + CourseInfoSection( + icon = Icons.Default.Info, + title = stringResource(id = R.string.core_about_this_course), + content = uiState.course.shortDescription, + modifier = Modifier.testTag("section_about_course") ) - } else { - var webViewAlpha by remember { mutableFloatStateOf(0f) } - if (webViewAlpha == 0f) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 20.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(color = MaterialTheme.appColors.primary) - } - } - Surface( + } + + // Course Description + if (uiState.course.description.isNotBlank()) { + CourseInfoSection( + icon = Icons.Default.Description, + title = stringResource(id = R.string.core_course_description), + content = uiState.course.description, + modifier = Modifier.testTag("section_course_description") + ) + } + + // Course Overview (HTML WebView) + if (uiState.course.overview.isNotBlank()) { + Column( modifier = Modifier - .padding(top = 16.dp) .fillMaxWidth() - .alpha(webViewAlpha), - color = MaterialTheme.appColors.background + .padding(horizontal = 24.dp, vertical = 12.dp) ) { - CourseDescription( - modifier = webViewPadding, - apiHostUrl = apiHostUrl, - body = htmlBody, - onWebPageLoaded = { - webViewAlpha = 1f + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 8.dp) + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Assignment, + contentDescription = null, + tint = MaterialTheme.appColors.primary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(id = R.string.core_course_overview), + style = MaterialTheme.appTypography.titleSmall, + color = MaterialTheme.appColors.textFieldHint + ) + } + + if (isPreview) { + Text( + text = htmlBody, + modifier = Modifier.testTag("txt_course_overview"), + ) + } else { + var webViewAlpha by remember { mutableFloatStateOf(0f) } + if (webViewAlpha == 0f) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.appColors.primary) + } + } + Surface( + modifier = Modifier + .fillMaxWidth() + .alpha(webViewAlpha), + color = MaterialTheme.appColors.background + ) { + CourseDescription( + modifier = Modifier, + apiHostUrl = apiHostUrl, + body = htmlBody, + onWebPageLoaded = { + webViewAlpha = 1f + } + ) } - ) + } } } + + // Course Requirements + if (uiState.course.courseRequirement.isNotBlank()) { + CourseInfoSection( + icon = Icons.AutoMirrored.Filled.Rule, + title = stringResource(id = R.string.core_course_requirements), + content = uiState.course.courseRequirement, + modifier = Modifier.testTag("section_course_requirements") + ) + } + + // Learning Outcomes + if (uiState.course.learningOutcomes.isNotBlank()) { + CourseInfoSection( + icon = Icons.Default.TrackChanges, + title = stringResource(id = R.string.core_learning_outcomes), + content = uiState.course.learningOutcomes, + modifier = Modifier.testTag("section_learning_outcomes") + ) + } + + // Instructors Section + if (uiState.course.instructorsList.isNotEmpty()) { + InstructorsSection( + instructors = uiState.course.instructorsList, + modifier = Modifier.testTag("section_instructors") + ) + } } } } @@ -474,13 +554,6 @@ private fun CourseDetailNativeContent( EnrollOverLabel() Spacer(Modifier.height(24.dp)) } - Text( - modifier = Modifier.testTag("txt_course_short_description"), - text = course.shortDescription, - style = MaterialTheme.appTypography.labelSmall, - color = MaterialTheme.appColors.textPrimaryVariant - ) - Spacer(Modifier.height(16.dp)) Text( modifier = Modifier.testTag("txt_course_name"), text = course.name, @@ -552,13 +625,6 @@ private fun CourseDetailNativeContentLandscape( verticalArrangement = Arrangement.SpaceBetween ) { Column { - Text( - modifier = Modifier.testTag("txt_course_short_description"), - text = course.shortDescription, - style = MaterialTheme.appTypography.labelSmall, - color = MaterialTheme.appColors.textPrimaryVariant - ) - Spacer(Modifier.height(16.dp)) Text( modifier = Modifier.testTag("txt_course_name"), text = course.name, @@ -570,7 +636,7 @@ private fun CourseDetailNativeContentLandscape( modifier = Modifier.testTag("txt_course_org"), text = course.org, style = MaterialTheme.appTypography.labelMedium, - color = MaterialTheme.appColors.textAccent + color = MaterialTheme.appColors.primary ) Spacer(Modifier.height(42.dp)) } @@ -635,6 +701,193 @@ private fun NoInternetLabel() { ) } +@Composable +private fun CourseInfoSection( + icon: ImageVector, + title: String, + content: String, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 12.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = if (content.isNotBlank()) 8.dp else 0.dp) + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.appColors.primary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = title, + style = MaterialTheme.appTypography.titleSmall, + color = MaterialTheme.appColors.textFieldHint + ) + } + if (content.isNotBlank()) { + Text( + text = content, + style = MaterialTheme.appTypography.bodyMedium, + color = MaterialTheme.appColors.textPrimary + ) + } + } +} + +@Composable +private fun InstructorCard( + instructor: org.openedx.discovery.data.model.Instructor, + modifier: Modifier = Modifier +) { + Surface( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .border( + width = 1.dp, + color = MaterialTheme.appColors.cardViewBorder, + shape = RoundedCornerShape(12.dp) + ), + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.appColors.background, + elevation = 0.dp + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top + ) { + if (!instructor.image.isNullOrBlank()) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(instructor.image) + .crossfade(true) + .build(), + contentDescription = instructor.name, + contentScale = androidx.compose.ui.layout.ContentScale.Crop, + modifier = Modifier + .size(70.dp) + .clip(CircleShape) + .border(1.dp, MaterialTheme.appColors.textFieldHint, CircleShape) + ) + } else { + Box( + modifier = Modifier + .size(70.dp) + .clip(CircleShape) + .background(MaterialTheme.appColors.cardViewBorder), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Person, + contentDescription = null, + tint = MaterialTheme.appColors.textFieldHint, + modifier = Modifier.size(35.dp) + ) + } + } + + Spacer(modifier = Modifier.width(16.dp)) + + Column( + modifier = Modifier.weight(1f) + ) { + if (!instructor.name.isNullOrBlank()) { + Text( + text = instructor.name, + style = MaterialTheme.appTypography.titleMedium.copy( + fontSize = 18.sp + ), + color = MaterialTheme.appColors.textPrimary, + fontWeight = FontWeight.Bold + ) + } + + if (!instructor.title.isNullOrBlank()) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = instructor.title, + style = MaterialTheme.appTypography.bodyMedium, + color = MaterialTheme.appColors.textFieldHint + ) + } + + if (!instructor.organization.isNullOrBlank()) { + Spacer(modifier = Modifier.height(4.dp)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Domain, + contentDescription = null, + tint = MaterialTheme.appColors.primary, + modifier = Modifier.size(14.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = instructor.organization, + style = MaterialTheme.appTypography.bodyMedium, + color = MaterialTheme.appColors.primary + ) + } + } + } + } + + if (!instructor.bio.isNullOrBlank()) { + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = instructor.bio, + style = MaterialTheme.appTypography.bodyMedium, + color = MaterialTheme.appColors.textFieldHint + ) + } + } + } +} + +@Composable +private fun InstructorsSection( + instructors: List, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 12.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 8.dp) + ) { + Icon( + imageVector = Icons.Default.Group, + contentDescription = null, + tint = MaterialTheme.appColors.primary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(id = R.string.core_instructors), + style = MaterialTheme.appTypography.titleSmall, + color = MaterialTheme.appColors.textFieldHint + ) + } + + instructors.forEach { instructor -> + InstructorCard(instructor = instructor) + } + } +} + @Composable @SuppressLint("SetJavaScriptEnabled") private fun CourseDescription( @@ -764,7 +1017,19 @@ private val mockCourse = Course( end = "end", startDisplay = "startDisplay", startType = "startType", - overview = "", + overview = "

Overview content

", isEnrolled = false, duration = "30 Days", + courseRequirement = "", + description = "", + learningOutcomes = "", + instructorsList = listOf( + org.openedx.discovery.data.model.Instructor( + name = "Instructor 1", + title = "Title 1", + organization = "Organization 1", + bio = "Bio content for instructor 1", + image = null + ), + ) ) diff --git a/discovery/src/main/res/values/strings.xml b/discovery/src/main/res/values/strings.xml index ab45da1d7..70fac189f 100644 --- a/discovery/src/main/res/values/strings.xml +++ b/discovery/src/main/res/values/strings.xml @@ -23,6 +23,14 @@ Course Duration: %1$s Course Duration: Not specified + + About this Course + Course Description + Course Overview + Course Requirements + Learning Outcomes + Instructors + Found %s course on your request Found %s courses on your request From a272be054d102b805bc0b2ac61411f8f474caf2f Mon Sep 17 00:00:00 2001 From: Yogesh Bhagat Date: Fri, 21 Nov 2025 18:58:26 +0530 Subject: [PATCH 3/4] adjust spacing for course overview field --- .../presentation/detail/CourseDetailsFragment.kt | 9 ++++++++- .../presentation/detail/CourseDetailsViewModel.kt | 11 +++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsFragment.kt index f27b6f858..d4d01face 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsFragment.kt @@ -363,7 +363,14 @@ internal fun CourseDetailsScreen( } // Course Overview (HTML WebView) - if (uiState.course.overview.isNotBlank()) { + // Only show if overview has actual content (not just empty HTML/entities) + if (uiState.course.overview.isNotBlank() && + uiState.course.overview + .replace(Regex("<[^>]*>"), "") // Remove HTML tags + .replace(" ", " ") // Replace non-breaking spaces + .replace("&", "&") // Replace ampersands + .replace(Regex("\\s+"), "") // Remove all whitespace + .isNotEmpty()) { Column( modifier = Modifier .fillMaxWidth() diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModel.kt index b212c588f..8ebd7c966 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModel.kt @@ -112,9 +112,20 @@ class CourseDetailsViewModel( fun getCourseAboutBody(bgColor: ULong, textColor: ULong): String { val darkThemeStyle = "" val buff = StringBuffer().apply { From b8aab7d4ccf0b1542c748d1a5d2246702bd0ce48 Mon Sep 17 00:00:00 2001 From: Yogesh Bhagat Date: Fri, 21 Nov 2025 18:58:58 +0530 Subject: [PATCH 4/4] add database migration for new fields for course about screen --- .../main/java/org/openedx/app/di/AppModule.kt | 3 +- .../java/org/openedx/app/room/AppDatabase.kt | 2 +- .../openedx/app/room/DatabaseMigrations.kt | 56 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/openedx/app/room/DatabaseMigrations.kt diff --git a/app/src/main/java/org/openedx/app/di/AppModule.kt b/app/src/main/java/org/openedx/app/di/AppModule.kt index ce6e20cd9..9a182709b 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -17,6 +17,7 @@ import org.openedx.app.BuildConfig import org.openedx.app.PluginManager import org.openedx.app.data.storage.PreferencesManager import org.openedx.app.deeplink.DeepLinkRouter +import org.openedx.app.room.ALL_MIGRATIONS import org.openedx.app.room.AppDatabase import org.openedx.app.room.DATABASE_NAME import org.openedx.app.room.DatabaseManager @@ -139,7 +140,7 @@ val appModule = module { androidApplication(), AppDatabase::class.java, DATABASE_NAME - ).fallbackToDestructiveMigration() + ).addMigrations(*ALL_MIGRATIONS) .fallbackToDestructiveMigrationOnDowngrade() .build() } diff --git a/app/src/main/java/org/openedx/app/room/AppDatabase.kt b/app/src/main/java/org/openedx/app/room/AppDatabase.kt index 6aa46ed1f..3be9b3ee5 100644 --- a/app/src/main/java/org/openedx/app/room/AppDatabase.kt +++ b/app/src/main/java/org/openedx/app/room/AppDatabase.kt @@ -18,7 +18,7 @@ import org.openedx.discovery.data.converter.DiscoveryConverter import org.openedx.discovery.data.model.room.CourseEntity import org.openedx.discovery.data.storage.DiscoveryDao -const val DATABASE_VERSION = 1 +const val DATABASE_VERSION = 2 const val DATABASE_NAME = "OpenEdX_db" @Database( diff --git a/app/src/main/java/org/openedx/app/room/DatabaseMigrations.kt b/app/src/main/java/org/openedx/app/room/DatabaseMigrations.kt new file mode 100644 index 000000000..b2e4455ac --- /dev/null +++ b/app/src/main/java/org/openedx/app/room/DatabaseMigrations.kt @@ -0,0 +1,56 @@ +package org.openedx.app.room + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +/** + * Database Migrations + * + * This file contains all database migrations for the app. + * When adding new migrations: + * 1. Create a new MIGRATION_X_Y object + * 2. Add it to the ALL_MIGRATIONS array + * 3. Update DATABASE_VERSION in AppDatabase.kt + * + * Best Practices: + * - Never remove migrations that have been released + * - Keep migrations in chronological order + * - Use descriptive comments + * - Test migrations thoroughly before release + */ + +/** + * Migration from version 1 to version 2 + * + * Changes: + * - Adds new fields to course_discovery_table: + * - courseRequirement: Course prerequisites/requirements + * - description: Detailed course description + * - learningOutcomes: Expected learning outcomes + * - instructors: JSON string of instructor information + */ +val MIGRATION_1_2 = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + // Add new columns to course_discovery_table with default empty string values + db.execSQL("ALTER TABLE course_discovery_table ADD COLUMN courseRequirement TEXT NOT NULL DEFAULT ''") + db.execSQL("ALTER TABLE course_discovery_table ADD COLUMN description TEXT NOT NULL DEFAULT ''") + db.execSQL("ALTER TABLE course_discovery_table ADD COLUMN learningOutcomes TEXT NOT NULL DEFAULT ''") + db.execSQL("ALTER TABLE course_discovery_table ADD COLUMN instructors TEXT NOT NULL DEFAULT ''") + } +} + +/** + * All migrations in chronological order. + * This array is used by the database builder to apply migrations automatically. + * + * To add a new migration: + * - Create a new MIGRATION_X_Y object above + * - Add it to this array: arrayOf(MIGRATION_1_2, MIGRATION_2_3, ...) + */ +val ALL_MIGRATIONS = arrayOf( + MIGRATION_1_2 + // Add future migrations here, e.g.: + // MIGRATION_2_3, + // MIGRATION_3_4, +) +