Skip to content

Commit addb8a5

Browse files
author
Ahsan Arif
committed
fix: course load exception and removed ui result class
1 parent d28d821 commit addb8a5

File tree

10 files changed

+116
-120
lines changed

10 files changed

+116
-120
lines changed

core/src/main/java/org/openedx/core/domain/model/CourseInfoOverview.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ data class CourseInfoOverview(
1111
val number: String,
1212
val org: String,
1313
val start: Date?,
14-
val startDisplay: String,
14+
val startDisplay: String?,
1515
val startType: String,
1616
val end: Date?,
1717
val isSelfPaced: Boolean,
@@ -27,7 +27,7 @@ data class CourseInfoOverview(
2727
number = number,
2828
org = org,
2929
start = start,
30-
startDisplay = startDisplay,
30+
startDisplay = startDisplay ?: "",
3131
startType = startType,
3232
end = end,
3333
isSelfPaced = isSelfPaced,

core/src/main/java/org/openedx/core/ui/Result.kt

Lines changed: 0 additions & 36 deletions
This file was deleted.

course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,30 @@ class CourseRepository(
4747

4848
suspend fun getCourseStructureFlow(courseId: String, forceRefresh: Boolean = true): Flow<CourseStructure> =
4949
channelFlowWithAwait {
50-
// Send the local result first
51-
trySend(getCourseStructureFromCache(courseId))
52-
// Send the updated network result if needed
53-
if (networkConnection.isOnline() && forceRefresh) {
54-
trySend(getCourseStructure(courseId, true))
50+
var hasCourseStructure = false
51+
val cachedCourseStructure =
52+
courseStructure[courseId] ?: (courseDao.getCourseStructureById(courseId)
53+
?.mapToDomain())
54+
if (cachedCourseStructure != null) {
55+
hasCourseStructure = true
56+
trySend(cachedCourseStructure)
57+
}
58+
val fetchRemoteCourse = !hasCourseStructure || forceRefresh
59+
if (networkConnection.isOnline() && fetchRemoteCourse) {
60+
val response = api.getCourseStructure(
61+
"stale-if-error=0",
62+
"v4",
63+
preferencesManager.user?.username,
64+
courseId
65+
)
66+
courseDao.insertCourseStructureEntity(response.mapToRoomEntity())
67+
val courseDomainModel = response.mapToDomain()
68+
courseStructure[courseId] = courseDomainModel
69+
trySend(courseDomainModel)
70+
hasCourseStructure = true
71+
}
72+
if (!hasCourseStructure) {
73+
throw NoCachedDataException()
5574
}
5675
}
5776

course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class CourseInteractor(
1515
suspend fun getCourseStructureFlow(
1616
courseId: String,
1717
forceRefresh: Boolean = true
18-
): Flow<CourseStructure> {
18+
): Flow<CourseStructure?> {
1919
return repository.getCourseStructureFlow(courseId, forceRefresh)
2020
}
2121

@@ -30,7 +30,7 @@ class CourseInteractor(
3030
return repository.getCourseStructureFromCache(courseId)
3131
}
3232

33-
suspend fun getEnrollmentDetailsFlow(courseId: String): Flow<CourseEnrollmentDetails> {
33+
suspend fun getEnrollmentDetailsFlow(courseId: String): Flow<CourseEnrollmentDetails?> {
3434
return repository.getEnrollmentDetailsFlow(courseId)
3535
}
3636

course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt

Lines changed: 54 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import kotlinx.coroutines.flow.SharedFlow
1313
import kotlinx.coroutines.flow.StateFlow
1414
import kotlinx.coroutines.flow.asSharedFlow
1515
import kotlinx.coroutines.flow.asStateFlow
16+
import kotlinx.coroutines.flow.catch
1617
import kotlinx.coroutines.flow.combine
17-
import kotlinx.coroutines.flow.take
1818
import kotlinx.coroutines.flow.update
1919
import kotlinx.coroutines.launch
2020
import org.openedx.core.config.Config
@@ -38,9 +38,6 @@ import org.openedx.core.system.notifier.CourseOpenBlock
3838
import org.openedx.core.system.notifier.CourseStructureUpdated
3939
import org.openedx.core.system.notifier.RefreshDates
4040
import org.openedx.core.system.notifier.RefreshDiscussions
41-
import org.openedx.core.ui.Result
42-
import org.openedx.core.ui.asResult
43-
import org.openedx.core.ui.isLoading
4441
import org.openedx.core.worker.CalendarSyncScheduler
4542
import org.openedx.course.DatesShiftedSnackBar
4643
import org.openedx.course.domain.interactor.CourseInteractor
@@ -170,45 +167,40 @@ class CourseContainerViewModel(
170167
// If data is already loaded, do nothing
171168
if (_dataReady.value != null) return
172169

170+
_showProgress.value = true
171+
173172
viewModelScope.launch {
174-
val courseFlow = interactor.getCourseStructureFlow(courseId)
175-
val enrollmentFlow = interactor.getEnrollmentDetailsFlow(courseId)
176-
// Combining the first emission from both flows, ensuring they emit at least once before combining
177-
courseFlow.take(1).combine(enrollmentFlow.take(1)) { course, enrollment ->
178-
course to enrollment
179-
}.asResult().collect { result ->
180-
_showProgress.value = result.isLoading
181-
if (result is Result.Success) {
182-
result.data.let { (structure, enrollment) ->
183-
processCourseData(structure, enrollment)
173+
try {
174+
val courseStructureFlow = interactor.getCourseStructureFlow(courseId)
175+
.catch { e ->
176+
handleFetchError(e)
177+
emit(null)
184178
}
185-
}
186-
if (result is Result.Error) {
187-
result.exception?.let { e ->
188-
e.printStackTrace()
189-
if (isNetworkRelatedError(e)) {
190-
_errorMessage.value =
191-
resourceManager.getString(CoreR.string.core_error_no_connection)
192-
} else {
193-
_courseAccessStatus.value = CourseAccessError.UNKNOWN
194-
}
195-
} ?: run {
196-
_courseAccessStatus.value = CourseAccessError.UNKNOWN
179+
val courseDetailsFlow = interactor.getEnrollmentDetailsFlow(courseId)
180+
.catch { emit(null) }
181+
courseStructureFlow.combine(courseDetailsFlow) { courseStructure, courseEnrollmentDetails ->
182+
courseStructure to courseEnrollmentDetails
183+
}.collect { (courseStructure, courseEnrollmentDetails) ->
184+
when {
185+
courseEnrollmentDetails != null -> handleCourseEnrollment(courseEnrollmentDetails)
186+
courseStructure != null -> handleCourseStructureOnly(courseStructure)
187+
else -> _courseAccessStatus.value = CourseAccessError.UNKNOWN
197188
}
198189
}
190+
} catch (e: Exception) {
191+
handleFetchError(e)
199192
}
200193
}
201194
}
202195

203-
private fun processCourseData(
204-
courseStructure: CourseStructure,
205-
courseDetails: CourseEnrollmentDetails
206-
) {
196+
/**
197+
* Handles the scenario where [CourseEnrollmentDetails] is successfully fetched.
198+
*/
199+
private fun handleCourseEnrollment(courseDetails: CourseEnrollmentDetails) {
207200
_courseDetails = courseDetails
208201
courseName = courseDetails.courseInfoOverview.name
209-
val courseImage = courseDetails.courseInfoOverview.media?.image?.large
210-
?: courseStructure.media?.image?.large
211-
loadCourseImage(courseImage)
202+
loadCourseImage(courseDetails.courseInfoOverview.media?.image?.large)
203+
212204
if (courseDetails.hasAccess.isFalse()) {
213205
_dataReady.value = false
214206
_courseAccessStatus.value = when {
@@ -233,6 +225,35 @@ class CourseContainerViewModel(
233225
}
234226
}
235227

228+
/**
229+
* Handles the scenario where we only have [CourseStructure] but no enrollment details.
230+
*/
231+
private fun handleCourseStructureOnly(courseStructure: CourseStructure) {
232+
loadCourseImage(courseStructure.media?.image?.large)
233+
_courseAccessStatus.value = CourseAccessError.NONE
234+
_isNavigationEnabled.value = true
235+
_calendarSyncUIState.update { state ->
236+
state.copy(isCalendarSyncEnabled = isCalendarSyncEnabled())
237+
}
238+
if (resumeBlockId.isNotEmpty()) {
239+
viewModelScope.launch {
240+
delay(500L)
241+
courseNotifier.send(CourseOpenBlock(resumeBlockId))
242+
}
243+
}
244+
_dataReady.value = true
245+
}
246+
247+
private fun handleFetchError(e: Throwable) {
248+
e.printStackTrace()
249+
if (isNetworkRelatedError(e)) {
250+
_errorMessage.value = resourceManager.getString(CoreR.string.core_error_no_connection)
251+
} else {
252+
_courseAccessStatus.value = CourseAccessError.UNKNOWN
253+
}
254+
_showProgress.value = false
255+
}
256+
236257
private fun isNetworkRelatedError(e: Throwable): Boolean {
237258
return e.isInternetError() || e is NoCachedDataException
238259
}

course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import kotlinx.coroutines.flow.StateFlow
99
import kotlinx.coroutines.flow.asSharedFlow
1010
import kotlinx.coroutines.flow.asStateFlow
1111
import kotlinx.coroutines.flow.combine
12-
import kotlinx.coroutines.flow.take
1312
import kotlinx.coroutines.launch
1413
import org.openedx.core.BlockType
1514
import org.openedx.core.R
@@ -35,9 +34,6 @@ import org.openedx.core.system.notifier.CourseDatesShifted
3534
import org.openedx.core.system.notifier.CourseNotifier
3635
import org.openedx.core.system.notifier.CourseOpenBlock
3736
import org.openedx.core.system.notifier.CourseStructureUpdated
38-
import org.openedx.core.ui.Result
39-
import org.openedx.core.ui.asResult
40-
import org.openedx.core.ui.error
4137
import org.openedx.course.domain.interactor.CourseInteractor
4238
import org.openedx.course.presentation.CourseAnalytics
4339
import org.openedx.course.presentation.CourseAnalyticsEvent
@@ -192,26 +188,18 @@ class CourseOutlineViewModel(
192188
val courseStatusFlow = interactor.getCourseStatusFlow(courseId)
193189
val courseDatesFlow = interactor.getCourseDatesFlow(courseId)
194190
combine(
195-
courseStructureFlow.take(1),
196-
courseStatusFlow.take(1),
197-
courseDatesFlow.take(1)
191+
courseStructureFlow, courseStatusFlow, courseDatesFlow
198192
) { courseStructure, courseStatus, courseDatesResult ->
199193
Triple(courseStructure, courseStatus, courseDatesResult)
200-
}.asResult().collect {
201-
if (it is Result.Success) {
202-
it.data.let { (courseStructure, courseStatus, courseDatesResult) ->
203-
val blocks = courseStructure.blockData
204-
val datesBannerInfo = courseDatesResult.courseBanner
194+
}.collect { (courseStructure, courseStatus, courseDates) ->
195+
if (courseStructure == null) return@collect
196+
val blocks = courseStructure.blockData
197+
val datesBannerInfo = courseDates.courseBanner
205198

206-
checkIfCalendarOutOfDate(courseDatesResult.datesSection.values.flatten())
207-
updateOutdatedOfflineXBlocks(courseStructure)
199+
checkIfCalendarOutOfDate(courseDates.datesSection.values.flatten())
200+
updateOutdatedOfflineXBlocks(courseStructure)
208201

209-
initializeCourseData(blocks, courseStructure, courseStatus, datesBannerInfo)
210-
}
211-
}
212-
if (it is Result.Error) {
213-
handleCourseDataError(it.error)
214-
}
202+
initializeCourseData(blocks, courseStructure, courseStatus, datesBannerInfo)
215203
}
216204
} catch (e: Exception) {
217205
handleCourseDataError(e)

course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@ import org.openedx.core.domain.model.EnrollmentDetails
4141
import org.openedx.core.system.connection.NetworkConnection
4242
import org.openedx.core.system.notifier.CourseNotifier
4343
import org.openedx.core.system.notifier.CourseStructureUpdated
44-
import org.openedx.core.ui.Result
45-
import org.openedx.core.ui.asResult
4644
import org.openedx.core.worker.CalendarSyncScheduler
4745
import org.openedx.course.domain.interactor.CourseInteractor
4846
import org.openedx.course.presentation.CourseAnalytics
@@ -237,11 +235,11 @@ class CourseContainerViewModelTest {
237235
)
238236
every { networkConnection.isOnline() } returns true
239237
coEvery {
240-
interactor.getCourseStructureFlow(any(), any()).asResult()
241-
} returns flowOf(Result.Error(Exception()))
238+
interactor.getCourseStructureFlow(any(), any())
239+
} returns flowOf(courseStructure)
242240
coEvery {
243-
interactor.getEnrollmentDetailsFlow(any()).asResult()
244-
} returns flowOf(Result.Error(Exception()))
241+
interactor.getEnrollmentDetailsFlow(any())
242+
} throws Exception()
245243
every {
246244
analytics.logScreenEvent(
247245
CourseAnalyticsEvent.DASHBOARD.eventName,

course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ import org.openedx.core.presentation.CoreAnalyticsEvent
5555
import org.openedx.core.system.connection.NetworkConnection
5656
import org.openedx.core.system.notifier.CourseNotifier
5757
import org.openedx.core.system.notifier.CourseStructureUpdated
58-
import org.openedx.core.ui.asResult
5958
import org.openedx.course.domain.interactor.CourseInteractor
6059
import org.openedx.course.presentation.CourseAnalytics
6160
import org.openedx.course.presentation.CourseRouter
@@ -307,7 +306,7 @@ class CourseOutlineViewModelTest {
307306
coEvery { interactor.getCourseStructureFlow(any(), any()) } returns flowOf(courseStructure)
308307
every { networkConnection.isOnline() } returns true
309308
every { downloadDao.getAllDataFlow() } returns flow { emit(emptyList()) }
310-
coEvery { interactor.getCourseStatusFlow(any()).asResult() } throws Exception()
309+
coEvery { interactor.getCourseStatusFlow(any()) } throws Exception()
311310
val viewModel = CourseOutlineViewModel(
312311
"",
313312
"",

dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,18 @@ class AllEnrolledCoursesViewModel(
5858

5959
init {
6060
collectDiscoveryNotifier()
61-
loadCachedCourses()
62-
getCourses(currentFilter.value, showLoadingProgress = false)
61+
loadInitialCourses()
62+
}
63+
64+
private fun loadInitialCourses() {
65+
viewModelScope.launch {
66+
_uiState.update { it.copy(showProgress = true) }
67+
val cachedList = interactor.getEnrolledCoursesFromCache()
68+
if (cachedList.isNotEmpty()) {
69+
_uiState.update { it.copy(courses = cachedList.toList(), showProgress = false) }
70+
}
71+
getCourses(showLoadingProgress = false)
72+
}
6373
}
6474

6575
fun getCourses(courseStatusFilter: CourseStatusFilter? = null, showLoadingProgress: Boolean = true) {
@@ -107,13 +117,6 @@ class AllEnrolledCoursesViewModel(
107117
}
108118
}
109119

110-
private fun loadCachedCourses() {
111-
viewModelScope.launch {
112-
val cachedList = interactor.getEnrolledCoursesFromCache()
113-
_uiState.update { it.copy(courses = cachedList.toList()) }
114-
}
115-
}
116-
117120
private fun internalLoadingCourses(courseStatusFilter: CourseStatusFilter? = null) {
118121
if (courseStatusFilter != null) {
119122
page = 1

dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,22 @@ class DashboardGalleryViewModel(
6767
fun getCourses() {
6868
viewModelScope.launch {
6969
try {
70-
isLoading = true
71-
val courseEnrollments = fileUtil.getObjectFromFile<CourseEnrollments>()
72-
if (courseEnrollments == null) {
73-
_uiState.value = DashboardGalleryUIState.Empty
70+
val cachedCourseEnrollments = fileUtil.getObjectFromFile<CourseEnrollments>()
71+
if (cachedCourseEnrollments == null) {
72+
if (networkConnection.isOnline()) {
73+
_uiState.value = DashboardGalleryUIState.Loading
74+
} else {
75+
_uiState.value = DashboardGalleryUIState.Empty
76+
}
7477
} else {
7578
_uiState.value =
7679
DashboardGalleryUIState.Courses(
77-
courseEnrollments.mapToDomain(),
80+
cachedCourseEnrollments.mapToDomain(),
7881
corePreferences.isRelativeDatesEnabled
7982
)
8083
}
8184
if (networkConnection.isOnline()) {
85+
isLoading = true
8286
val pageSize = if (windowSize.isTablet) {
8387
PAGE_SIZE_TABLET
8488
} else {

0 commit comments

Comments
 (0)