Skip to content

Commit 87b3517

Browse files
feat: added dates request
1 parent a263d94 commit 87b3517

File tree

9 files changed

+205
-27
lines changed

9 files changed

+205
-27
lines changed

app/src/main/java/org/openedx/app/di/ScreenModule.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import org.openedx.courses.presentation.DashboardGalleryViewModel
3737
import org.openedx.dashboard.data.repository.DashboardRepository
3838
import org.openedx.dashboard.domain.interactor.DashboardInteractor
3939
import org.openedx.dashboard.presentation.DashboardListViewModel
40+
import org.openedx.dates.data.repository.DatesRepository
41+
import org.openedx.dates.domain.interactor.DatesInteractor
4042
import org.openedx.dates.presentation.dates.DatesViewModel
4143
import org.openedx.discovery.data.repository.DiscoveryRepository
4244
import org.openedx.discovery.domain.interactor.DiscoveryInteractor
@@ -493,10 +495,23 @@ val screenModule = module {
493495
)
494496
}
495497

498+
factory {
499+
DatesRepository(
500+
api = get(),
501+
preferencesManager = get()
502+
)
503+
}
504+
factory {
505+
DatesInteractor(
506+
repository = get()
507+
)
508+
}
496509
viewModel {
497510
DatesViewModel(
498511
datesRouter = get(),
499-
networkConnection = get()
512+
networkConnection = get(),
513+
resourceManager = get(),
514+
datesInteractor = get()
500515
)
501516
}
502517
}

core/src/main/java/org/openedx/core/data/api/CourseApi.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import org.openedx.core.data.model.BlocksCompletionBody
66
import org.openedx.core.data.model.CourseComponentStatus
77
import org.openedx.core.data.model.CourseDates
88
import org.openedx.core.data.model.CourseDatesBannerInfo
9+
import org.openedx.core.data.model.CourseDatesResponse
910
import org.openedx.core.data.model.CourseEnrollmentDetails
1011
import org.openedx.core.data.model.CourseEnrollments
1112
import org.openedx.core.data.model.CourseStructureModel
@@ -103,4 +104,10 @@ interface CourseApi {
103104
suspend fun getEnrollmentDetails(
104105
@Path("course_id") courseId: String,
105106
): CourseEnrollmentDetails
107+
108+
@GET("/api/mobile/v1/course_dates/{username}/")
109+
suspend fun getUserDates(
110+
@Path("username") username: String,
111+
@Query("page") page: Int = 1
112+
): CourseDatesResponse
106113
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.openedx.core.data.model
2+
3+
import com.google.gson.annotations.SerializedName
4+
import org.openedx.core.utils.TimeUtils
5+
import org.openedx.core.domain.model.CourseDate as DomainCourseDate
6+
import org.openedx.core.domain.model.CourseDatesResponse as DomainCourseDatesResponse
7+
8+
data class CourseDate(
9+
@SerializedName("course_id")
10+
val courseId: String,
11+
@SerializedName("assignment_block_id")
12+
val assignmentBlockId: String,
13+
@SerializedName("due_date")
14+
val dueDate: String?,
15+
@SerializedName("assignment_title")
16+
val assignmentTitle: String?,
17+
@SerializedName("learner_has_access")
18+
val learnerHasAccess: Boolean?,
19+
@SerializedName("course_name")
20+
val courseName: String?
21+
) {
22+
fun mapToDomain(): DomainCourseDate? {
23+
val dueDate = TimeUtils.iso8601ToDate(dueDate ?: "")
24+
return DomainCourseDate(
25+
courseId = courseId,
26+
assignmentBlockId = assignmentBlockId,
27+
dueDate = dueDate ?: return null,
28+
assignmentTitle = assignmentTitle ?: "",
29+
learnerHasAccess = learnerHasAccess ?: false,
30+
courseName = courseName ?: ""
31+
)
32+
}
33+
}
34+
35+
data class CourseDatesResponse(
36+
@SerializedName("count")
37+
val count: Int,
38+
@SerializedName("next")
39+
val next: Int?,
40+
@SerializedName("previous")
41+
val previous: Int?,
42+
@SerializedName("results")
43+
val results: List<CourseDate>
44+
) {
45+
fun mapToDomain(): DomainCourseDatesResponse {
46+
return DomainCourseDatesResponse(
47+
count = count,
48+
next = next,
49+
previous = previous,
50+
results = results.mapNotNull { it.mapToDomain() }
51+
)
52+
}
53+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.openedx.core.domain.model
2+
3+
import java.util.Date
4+
5+
data class CourseDatesResponse(
6+
val count: Int,
7+
val next: Int?,
8+
val previous: Int?,
9+
val results: List<CourseDate>
10+
)
11+
12+
data class CourseDate(
13+
val courseId: String,
14+
val assignmentBlockId: String,
15+
val dueDate: Date,
16+
val assignmentTitle: String,
17+
val learnerHasAccess: Boolean,
18+
val courseName: String
19+
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.openedx.dates.data.repository
2+
3+
import org.openedx.core.data.api.CourseApi
4+
import org.openedx.core.data.storage.CorePreferences
5+
import org.openedx.core.domain.model.CourseDatesResponse
6+
7+
class DatesRepository(
8+
private val api: CourseApi,
9+
private val preferencesManager: CorePreferences
10+
) {
11+
suspend fun getUserDates(): CourseDatesResponse {
12+
val username = preferencesManager.user?.username ?: ""
13+
return api.getUserDates(username).mapToDomain()
14+
}
15+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.openedx.dates.domain.interactor
2+
3+
import org.openedx.dates.data.repository.DatesRepository
4+
5+
class DatesInteractor(
6+
private val repository: DatesRepository
7+
) {
8+
9+
suspend fun getUserDates() = repository.getUserDates()
10+
11+
}

dates/src/main/java/org/openedx/dates/presentation/dates/DatesFragment.kt

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import androidx.compose.foundation.layout.width
2222
import androidx.compose.foundation.layout.widthIn
2323
import androidx.compose.foundation.lazy.LazyColumn
2424
import androidx.compose.foundation.lazy.itemsIndexed
25+
import androidx.compose.foundation.rememberScrollState
2526
import androidx.compose.foundation.shape.RoundedCornerShape
27+
import androidx.compose.foundation.verticalScroll
2628
import androidx.compose.material.CircularProgressIndicator
2729
import androidx.compose.material.ExperimentalMaterialApi
2830
import androidx.compose.material.Icon
@@ -47,6 +49,7 @@ import androidx.compose.ui.Modifier
4749
import androidx.compose.ui.graphics.Color
4850
import androidx.compose.ui.graphics.RectangleShape
4951
import androidx.compose.ui.platform.ComposeView
52+
import androidx.compose.ui.platform.LocalContext
5053
import androidx.compose.ui.platform.ViewCompositionStrategy
5154
import androidx.compose.ui.platform.testTag
5255
import androidx.compose.ui.res.painterResource
@@ -57,6 +60,7 @@ import androidx.compose.ui.unit.Dp
5760
import androidx.compose.ui.unit.dp
5861
import androidx.fragment.app.Fragment
5962
import org.koin.androidx.viewmodel.ext.android.viewModel
63+
import org.openedx.core.domain.model.CourseDate
6064
import org.openedx.core.presentation.ListItemPosition
6165
import org.openedx.core.ui.HandleUIMessage
6266
import org.openedx.core.ui.MainScreenTitle
@@ -66,6 +70,7 @@ import org.openedx.core.ui.statusBarsInset
6670
import org.openedx.core.ui.theme.OpenEdXTheme
6771
import org.openedx.core.ui.theme.appColors
6872
import org.openedx.core.ui.theme.appTypography
73+
import org.openedx.core.utils.TimeUtils
6974
import org.openedx.dates.R
7075
import org.openedx.foundation.presentation.UIMessage
7176
import org.openedx.foundation.presentation.rememberWindowSize
@@ -166,7 +171,8 @@ private fun DatesScreen(
166171
) {
167172
if (uiState.isLoading) {
168173
Box(
169-
modifier = Modifier.fillMaxSize(),
174+
modifier = Modifier
175+
.fillMaxSize(),
170176
contentAlignment = Alignment.Center
171177
) {
172178
CircularProgressIndicator(color = MaterialTheme.appColors.primary)
@@ -203,7 +209,7 @@ private fun DatesScreen(
203209
val itemPosition =
204210
ListItemPosition.detectPosition(index, dates)
205211
DateItem(
206-
date = date,
212+
courseDate = date,
207213
lineColor = dueDateCategory.color,
208214
itemPosition = itemPosition,
209215
onClick = {
@@ -247,11 +253,12 @@ private fun DatesScreen(
247253
@Composable
248254
private fun DateItem(
249255
modifier: Modifier = Modifier,
250-
date: String,
256+
courseDate: CourseDate,
251257
lineColor: Color,
252258
itemPosition: ListItemPosition,
253259
onClick: () -> Unit,
254260
) {
261+
val context = LocalContext.current
255262
val boxCornerWidth = 8.dp
256263
val boxCornerRadius = boxCornerWidth / 2
257264
val infoPadding = 8.dp
@@ -313,7 +320,7 @@ private fun DateItem(
313320
verticalArrangement = Arrangement.spacedBy(4.dp)
314321
) {
315322
Text(
316-
text = date,
323+
text = TimeUtils.formatToString(context, courseDate.dueDate, true),
317324
style = MaterialTheme.appTypography.labelMedium,
318325
color = MaterialTheme.appColors.textDark
319326
)
@@ -326,13 +333,13 @@ private fun DateItem(
326333
)
327334
Spacer(modifier = Modifier.width(4.dp))
328335
Text(
329-
text = date,
336+
text = courseDate.assignmentTitle,
330337
style = MaterialTheme.appTypography.titleMedium,
331338
color = MaterialTheme.appColors.textDark
332339
)
333340
}
334341
Text(
335-
text = date,
342+
text = courseDate.courseName,
336343
style = MaterialTheme.appTypography.labelMedium,
337344
color = MaterialTheme.appColors.textPrimaryVariant
338345
)
@@ -352,7 +359,9 @@ private fun EmptyState(
352359
modifier: Modifier = Modifier
353360
) {
354361
Box(
355-
modifier = modifier.fillMaxSize(),
362+
modifier = modifier
363+
.fillMaxSize()
364+
.verticalScroll(rememberScrollState()),
356365
contentAlignment = Alignment.Center
357366
) {
358367
Column(
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package org.openedx.dates.presentation.dates
22

3+
import org.openedx.core.domain.model.CourseDate
4+
35
data class DatesUIState(
46
val isLoading: Boolean = true,
57
val isRefreshing: Boolean = false,
6-
val dates: Map<DueDateCategory, List<String>> = emptyMap()
8+
val dates: Map<DueDateCategory, List<CourseDate>> = emptyMap()
79
)

dates/src/main/java/org/openedx/dates/presentation/dates/DatesViewModel.kt

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,26 @@ import kotlinx.coroutines.flow.asSharedFlow
1010
import kotlinx.coroutines.flow.asStateFlow
1111
import kotlinx.coroutines.flow.update
1212
import kotlinx.coroutines.launch
13+
import org.openedx.core.R
14+
import org.openedx.core.domain.model.CourseDate
15+
import org.openedx.core.domain.model.CourseDatesResponse
1316
import org.openedx.core.system.connection.NetworkConnection
17+
import org.openedx.core.utils.isToday
18+
import org.openedx.core.utils.toCalendar
19+
import org.openedx.dates.domain.interactor.DatesInteractor
1420
import org.openedx.dates.presentation.DatesRouter
21+
import org.openedx.foundation.extension.isInternetError
1522
import org.openedx.foundation.presentation.BaseViewModel
1623
import org.openedx.foundation.presentation.UIMessage
24+
import org.openedx.foundation.system.ResourceManager
25+
import java.util.Calendar
26+
import java.util.Date
1727

1828
class DatesViewModel(
1929
private val datesRouter: DatesRouter,
2030
private val networkConnection: NetworkConnection,
31+
private val resourceManager: ResourceManager,
32+
private val datesInteractor: DatesInteractor
2133
) : BaseViewModel() {
2234

2335
private val _uiState = MutableStateFlow(DatesUIState())
@@ -32,38 +44,73 @@ class DatesViewModel(
3244
get() = networkConnection.isOnline()
3345

3446
init {
35-
fetchDates()
47+
fetchDates(false)
3648
}
3749

38-
private fun fetchDates() {
50+
private fun fetchDates(refresh: Boolean) {
3951
viewModelScope.launch {
40-
_uiState.update { state ->
41-
state.copy(
42-
isLoading = false,
43-
isRefreshing = false,
44-
dates = mapOf(
45-
DueDateCategory.PAST_DUE to listOf("Date1", "Date2", "Date3"),
46-
DueDateCategory.TODAY to listOf("Date1"),
47-
DueDateCategory.THIS_WEEK to listOf("Date1", "Date2"),
48-
DueDateCategory.UPCOMING to listOf("Date1", "Date2", "Date3", "Date4"),
52+
try {
53+
_uiState.update { state ->
54+
state.copy(
55+
isLoading = !refresh,
56+
isRefreshing = refresh,
4957
)
50-
)
58+
}
59+
val courseDatesResponse = datesInteractor.getUserDates()
60+
_uiState.update { state ->
61+
state.copy(
62+
isLoading = false,
63+
isRefreshing = false,
64+
dates = groupCourseDates(courseDatesResponse)
65+
)
66+
}
67+
} catch (e: Exception) {
68+
if (e.isInternetError()) {
69+
_uiMessage.emit(
70+
UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))
71+
)
72+
} else {
73+
_uiMessage.emit(
74+
UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error))
75+
)
76+
}
5177
}
5278
}
5379
}
5480

5581
fun refreshData() {
56-
_uiState.update { state ->
57-
state.copy(
58-
isRefreshing = true,
59-
)
60-
}
61-
fetchDates()
82+
fetchDates(true)
6283
}
6384

6485
fun onSettingsClick(fragmentManager: FragmentManager) {
6586
datesRouter.navigateToSettings(fragmentManager)
6687
}
88+
89+
private fun groupCourseDates(response: CourseDatesResponse): Map<DueDateCategory, List<CourseDate>> {
90+
val now = Date()
91+
val calNow = Calendar.getInstance().apply { time = now }
92+
val grouped = response.results.groupBy { courseDate ->
93+
val dueDate = courseDate.dueDate
94+
if (dueDate.before(now)) {
95+
DueDateCategory.PAST_DUE
96+
} else if (dueDate.isToday()) {
97+
DueDateCategory.TODAY
98+
} else {
99+
val calDue = dueDate.toCalendar()
100+
val weekNow = calNow.get(Calendar.WEEK_OF_YEAR)
101+
val weekDue = calDue.get(Calendar.WEEK_OF_YEAR)
102+
val yearNow = calNow.get(Calendar.YEAR)
103+
val yearDue = calDue.get(Calendar.YEAR)
104+
if (weekNow == weekDue && yearNow == yearDue) {
105+
DueDateCategory.THIS_WEEK
106+
} else {
107+
DueDateCategory.UPCOMING
108+
}
109+
}
110+
}
111+
112+
return grouped
113+
}
67114
}
68115

69116
interface DatesViewActions {

0 commit comments

Comments
 (0)