Skip to content

Commit 4df0b02

Browse files
feat: [FC-0092] Course progress tab (#448)
* feat: progress tab UI * feat: progress tab logic * feat: cache first logic * feat: color coding * fix: changes according demo feedback * fix: changes according demo feedback * feat: color coding * feat: separator between assignment policies on the progress * feat: progressTabClickedEvent * feat: defaultColors changed
1 parent 6880aaa commit 4df0b02

File tree

28 files changed

+2656
-48
lines changed

28 files changed

+2656
-48
lines changed

app/schemas/org.openedx.app.room.AppDatabase/3.json

Lines changed: 1198 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import org.openedx.course.presentation.dates.CourseDatesViewModel
2323
import org.openedx.course.presentation.handouts.HandoutsViewModel
2424
import org.openedx.course.presentation.offline.CourseOfflineViewModel
2525
import org.openedx.course.presentation.outline.CourseOutlineViewModel
26+
import org.openedx.course.presentation.progress.CourseProgressViewModel
2627
import org.openedx.course.presentation.section.CourseSectionViewModel
2728
import org.openedx.course.presentation.unit.container.CourseUnitContainerViewModel
2829
import org.openedx.course.presentation.unit.html.HtmlUnitViewModel
@@ -495,6 +496,13 @@ val screenModule = module {
495496
get(),
496497
)
497498
}
499+
viewModel { (courseId: String) ->
500+
CourseProgressViewModel(
501+
courseId,
502+
get(),
503+
get()
504+
)
505+
}
498506

499507
single {
500508
DownloadRepository(

app/src/main/java/org/openedx/app/room/AppDatabase.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import androidx.room.TypeConverters
77
import org.openedx.core.data.model.room.CourseCalendarEventEntity
88
import org.openedx.core.data.model.room.CourseCalendarStateEntity
99
import org.openedx.core.data.model.room.CourseEnrollmentDetailsEntity
10+
import org.openedx.core.data.model.room.CourseProgressEntity
1011
import org.openedx.core.data.model.room.CourseStructureEntity
1112
import org.openedx.core.data.model.room.DownloadCoursePreview
1213
import org.openedx.core.data.model.room.OfflineXBlockProgress
@@ -21,7 +22,7 @@ import org.openedx.discovery.data.converter.DiscoveryConverter
2122
import org.openedx.discovery.data.model.room.CourseEntity
2223
import org.openedx.discovery.data.storage.DiscoveryDao
2324

24-
const val DATABASE_VERSION = 2
25+
const val DATABASE_VERSION = 3
2526
const val DATABASE_NAME = "OpenEdX_db"
2627

2728
@Database(
@@ -34,10 +35,12 @@ const val DATABASE_NAME = "OpenEdX_db"
3435
CourseCalendarEventEntity::class,
3536
CourseCalendarStateEntity::class,
3637
DownloadCoursePreview::class,
37-
CourseEnrollmentDetailsEntity::class
38+
CourseEnrollmentDetailsEntity::class,
39+
CourseProgressEntity::class,
3840
],
3941
autoMigrations = [
40-
AutoMigration(1, DATABASE_VERSION)
42+
AutoMigration(1, 2),
43+
AutoMigration(2, DATABASE_VERSION),
4144
],
4245
version = DATABASE_VERSION
4346
)

app/src/main/java/org/openedx/app/room/DatabaseManager.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ class DatabaseManager(
1717
) : DatabaseManager {
1818
override fun clearTables() {
1919
CoroutineScope(Dispatchers.IO).launch {
20-
courseDao.clearCachedData()
21-
courseDao.clearEnrollmentCachedData()
20+
courseDao.clearCourseData()
2221
dashboardDao.clearCachedData()
2322
downloadDao.clearOfflineProgress()
2423
discoveryDao.clearCachedData()

core/src/main/java/org/openedx/core/NoContentScreenType.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,9 @@ enum class NoContentScreenType(
2727
COURSE_ANNOUNCEMENTS(
2828
iconResId = R.drawable.core_ic_no_announcements,
2929
messageResId = R.string.core_no_announcements
30-
)
30+
),
31+
COURSE_PROGRESS(
32+
iconResId = R.drawable.core_ic_no_content,
33+
messageResId = R.string.core_no_progress
34+
),
3135
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import org.openedx.core.data.model.CourseDates
88
import org.openedx.core.data.model.CourseDatesBannerInfo
99
import org.openedx.core.data.model.CourseEnrollmentDetails
1010
import org.openedx.core.data.model.CourseEnrollments
11+
import org.openedx.core.data.model.CourseProgressResponse
1112
import org.openedx.core.data.model.CourseStructureModel
1213
import org.openedx.core.data.model.DownloadCoursePreview
1314
import org.openedx.core.data.model.EnrollmentStatus
@@ -109,4 +110,9 @@ interface CourseApi {
109110
suspend fun getDownloadCoursesPreview(
110111
@Path("username") username: String
111112
): List<DownloadCoursePreview>
113+
114+
@GET("/api/course_home/progress/{course_id}")
115+
suspend fun getCourseProgress(
116+
@Path("course_id") courseId: String,
117+
): CourseProgressResponse
112118
}
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
package org.openedx.core.data.model
2+
3+
import androidx.compose.ui.graphics.Color
4+
import androidx.core.graphics.toColorInt
5+
import com.google.gson.annotations.SerializedName
6+
import org.openedx.core.data.model.room.CertificateDataDb
7+
import org.openedx.core.data.model.room.CompletionSummaryDb
8+
import org.openedx.core.data.model.room.CourseGradeDb
9+
import org.openedx.core.data.model.room.CourseProgressEntity
10+
import org.openedx.core.data.model.room.GradingPolicyDb
11+
import org.openedx.core.data.model.room.SectionScoreDb
12+
import org.openedx.core.data.model.room.VerificationDataDb
13+
import org.openedx.core.domain.model.CourseProgress
14+
15+
data class CourseProgressResponse(
16+
@SerializedName("verified_mode") val verifiedMode: String?,
17+
@SerializedName("access_expiration") val accessExpiration: String?,
18+
@SerializedName("certificate_data") val certificateData: CertificateData?,
19+
@SerializedName("completion_summary") val completionSummary: CompletionSummary?,
20+
@SerializedName("course_grade") val courseGrade: CourseGrade?,
21+
@SerializedName("credit_course_requirements") val creditCourseRequirements: String?,
22+
@SerializedName("end") val end: String?,
23+
@SerializedName("enrollment_mode") val enrollmentMode: String?,
24+
@SerializedName("grading_policy") val gradingPolicy: GradingPolicy?,
25+
@SerializedName("has_scheduled_content") val hasScheduledContent: Boolean?,
26+
@SerializedName("section_scores") val sectionScores: List<SectionScore>?,
27+
@SerializedName("studio_url") val studioUrl: String?,
28+
@SerializedName("username") val username: String?,
29+
@SerializedName("user_has_passing_grade") val userHasPassingGrade: Boolean?,
30+
@SerializedName("verification_data") val verificationData: VerificationData?,
31+
@SerializedName("disable_progress_graph") val disableProgressGraph: Boolean?,
32+
) {
33+
data class CertificateData(
34+
@SerializedName("cert_status") val certStatus: String?,
35+
@SerializedName("cert_web_view_url") val certWebViewUrl: String?,
36+
@SerializedName("download_url") val downloadUrl: String?,
37+
@SerializedName("certificate_available_date") val certificateAvailableDate: String?
38+
) {
39+
fun mapToRoomEntity() = CertificateDataDb(
40+
certStatus = certStatus.orEmpty(),
41+
certWebViewUrl = certWebViewUrl.orEmpty(),
42+
downloadUrl = downloadUrl.orEmpty(),
43+
certificateAvailableDate = certificateAvailableDate.orEmpty()
44+
)
45+
46+
fun mapToDomain() = CourseProgress.CertificateData(
47+
certStatus = certStatus ?: "",
48+
certWebViewUrl = certWebViewUrl ?: "",
49+
downloadUrl = downloadUrl ?: "",
50+
certificateAvailableDate = certificateAvailableDate ?: ""
51+
)
52+
}
53+
54+
data class CompletionSummary(
55+
@SerializedName("complete_count") val completeCount: Int?,
56+
@SerializedName("incomplete_count") val incompleteCount: Int?,
57+
@SerializedName("locked_count") val lockedCount: Int?
58+
) {
59+
fun mapToRoomEntity() = CompletionSummaryDb(
60+
completeCount = completeCount ?: 0,
61+
incompleteCount = incompleteCount ?: 0,
62+
lockedCount = lockedCount ?: 0
63+
)
64+
65+
fun mapToDomain() = CourseProgress.CompletionSummary(
66+
completeCount = completeCount ?: 0,
67+
incompleteCount = incompleteCount ?: 0,
68+
lockedCount = lockedCount ?: 0
69+
)
70+
}
71+
72+
data class CourseGrade(
73+
@SerializedName("letter_grade") val letterGrade: String?,
74+
@SerializedName("percent") val percent: Double?,
75+
@SerializedName("is_passing") val isPassing: Boolean?
76+
) {
77+
fun mapToRoomEntity() = CourseGradeDb(
78+
letterGrade = letterGrade.orEmpty(),
79+
percent = percent ?: 0.0,
80+
isPassing = isPassing ?: false
81+
)
82+
83+
fun mapToDomain() = CourseProgress.CourseGrade(
84+
letterGrade = letterGrade ?: "",
85+
percent = percent ?: 0.0,
86+
isPassing = isPassing ?: false
87+
)
88+
}
89+
90+
data class GradingPolicy(
91+
@SerializedName("assignment_policies") val assignmentPolicies: List<AssignmentPolicy>?,
92+
@SerializedName("grade_range") val gradeRange: Map<String, Float>?,
93+
@SerializedName("assignment_colors") val assignmentColors: List<String>?
94+
) {
95+
// TODO Temporary solution. Backend will returns color list later
96+
val defaultColors = listOf(
97+
"#D24242",
98+
"#7B9645",
99+
"#5A5AD8",
100+
"#B0842C",
101+
"#2E90C2",
102+
"#D13F88",
103+
"#36A17D",
104+
"#AE5AD8",
105+
"#3BA03B"
106+
)
107+
108+
fun mapToRoomEntity() = GradingPolicyDb(
109+
assignmentPolicies = assignmentPolicies?.map { it.mapToRoomEntity() } ?: emptyList(),
110+
gradeRange = gradeRange ?: emptyMap(),
111+
assignmentColors = assignmentColors ?: defaultColors
112+
)
113+
114+
fun mapToDomain() = CourseProgress.GradingPolicy(
115+
assignmentPolicies = assignmentPolicies?.map { it.mapToDomain() } ?: emptyList(),
116+
gradeRange = gradeRange ?: emptyMap(),
117+
assignmentColors = assignmentColors?.map { colorString ->
118+
Color(colorString.toColorInt())
119+
} ?: defaultColors.map { Color(it.toColorInt()) }
120+
)
121+
122+
data class AssignmentPolicy(
123+
@SerializedName("num_droppable") val numDroppable: Int?,
124+
@SerializedName("num_total") val numTotal: Int?,
125+
@SerializedName("short_label") val shortLabel: String?,
126+
@SerializedName("type") val type: String?,
127+
@SerializedName("weight") val weight: Double?
128+
) {
129+
fun mapToRoomEntity() = GradingPolicyDb.AssignmentPolicyDb(
130+
numDroppable = numDroppable ?: 0,
131+
numTotal = numTotal ?: 0,
132+
shortLabel = shortLabel.orEmpty(),
133+
type = type.orEmpty(),
134+
weight = weight ?: 0.0
135+
)
136+
137+
fun mapToDomain() = CourseProgress.GradingPolicy.AssignmentPolicy(
138+
numDroppable = numDroppable ?: 0,
139+
numTotal = numTotal ?: 0,
140+
shortLabel = shortLabel ?: "",
141+
type = type ?: "",
142+
weight = weight ?: 0.0
143+
)
144+
}
145+
}
146+
147+
data class SectionScore(
148+
@SerializedName("display_name") val displayName: String?,
149+
@SerializedName("subsections") val subsections: List<Subsection>?
150+
) {
151+
fun mapToRoomEntity() = SectionScoreDb(
152+
displayName = displayName.orEmpty(),
153+
subsections = subsections?.map { it.mapToRoomEntity() } ?: emptyList()
154+
)
155+
156+
fun mapToDomain() = CourseProgress.SectionScore(
157+
displayName = displayName ?: "",
158+
subsections = subsections?.map { it.mapToDomain() } ?: emptyList()
159+
)
160+
data class Subsection(
161+
@SerializedName("assignment_type") val assignmentType: String?,
162+
@SerializedName("block_key") val blockKey: String?,
163+
@SerializedName("display_name") val displayName: String?,
164+
@SerializedName("has_graded_assignment") val hasGradedAssignment: Boolean?,
165+
@SerializedName("override") val override: String?,
166+
@SerializedName("learner_has_access") val learnerHasAccess: Boolean?,
167+
@SerializedName("num_points_earned") val numPointsEarned: Float?,
168+
@SerializedName("num_points_possible") val numPointsPossible: Float?,
169+
@SerializedName("percent_graded") val percentGraded: Double?,
170+
@SerializedName("problem_scores") val problemScores: List<ProblemScore>?,
171+
@SerializedName("show_correctness") val showCorrectness: String?,
172+
@SerializedName("show_grades") val showGrades: Boolean?,
173+
@SerializedName("url") val url: String?
174+
) {
175+
fun mapToRoomEntity() = SectionScoreDb.SubsectionDb(
176+
assignmentType = assignmentType.orEmpty(),
177+
blockKey = blockKey.orEmpty(),
178+
displayName = displayName.orEmpty(),
179+
hasGradedAssignment = hasGradedAssignment ?: false,
180+
override = override.orEmpty(),
181+
learnerHasAccess = learnerHasAccess ?: false,
182+
numPointsEarned = numPointsEarned ?: 0f,
183+
numPointsPossible = numPointsPossible ?: 0f,
184+
percentGraded = percentGraded ?: 0.0,
185+
problemScores = problemScores?.map { it.mapToRoomEntity() } ?: emptyList(),
186+
showCorrectness = showCorrectness.orEmpty(),
187+
showGrades = showGrades ?: false,
188+
url = url.orEmpty()
189+
)
190+
191+
fun mapToDomain() = CourseProgress.SectionScore.Subsection(
192+
assignmentType = assignmentType ?: "",
193+
blockKey = blockKey ?: "",
194+
displayName = displayName ?: "",
195+
hasGradedAssignment = hasGradedAssignment ?: false,
196+
override = override ?: "",
197+
learnerHasAccess = learnerHasAccess ?: false,
198+
numPointsEarned = numPointsEarned ?: 0f,
199+
numPointsPossible = numPointsPossible ?: 0f,
200+
percentGraded = percentGraded ?: 0.0,
201+
problemScores = problemScores?.map { it.mapToDomain() } ?: emptyList(),
202+
showCorrectness = showCorrectness ?: "",
203+
showGrades = showGrades ?: false,
204+
url = url ?: ""
205+
)
206+
data class ProblemScore(
207+
@SerializedName("earned") val earned: Double?,
208+
@SerializedName("possible") val possible: Double?
209+
) {
210+
fun mapToRoomEntity() = SectionScoreDb.SubsectionDb.ProblemScoreDb(
211+
earned = earned ?: 0.0,
212+
possible = possible ?: 0.0
213+
)
214+
215+
fun mapToDomain() = CourseProgress.SectionScore.Subsection.ProblemScore(
216+
earned = earned ?: 0.0,
217+
possible = possible ?: 0.0
218+
)
219+
}
220+
}
221+
}
222+
223+
data class VerificationData(
224+
@SerializedName("link") val link: String?,
225+
@SerializedName("status") val status: String?,
226+
@SerializedName("status_date") val statusDate: String?
227+
) {
228+
fun mapToRoomEntity() = VerificationDataDb(
229+
link = link.orEmpty(),
230+
status = status.orEmpty(),
231+
statusDate = statusDate.orEmpty()
232+
)
233+
234+
fun mapToDomain() = CourseProgress.VerificationData(
235+
link = link ?: "",
236+
status = status ?: "",
237+
statusDate = statusDate ?: ""
238+
)
239+
}
240+
241+
fun mapToDomain(): CourseProgress {
242+
return CourseProgress(
243+
verifiedMode = verifiedMode ?: "",
244+
accessExpiration = accessExpiration ?: "",
245+
certificateData = certificateData?.mapToDomain(),
246+
completionSummary = completionSummary?.mapToDomain(),
247+
courseGrade = courseGrade?.mapToDomain(),
248+
creditCourseRequirements = creditCourseRequirements ?: "",
249+
end = end ?: "",
250+
enrollmentMode = enrollmentMode ?: "",
251+
gradingPolicy = gradingPolicy?.mapToDomain(),
252+
hasScheduledContent = hasScheduledContent ?: false,
253+
sectionScores = sectionScores?.map { it.mapToDomain() } ?: emptyList(),
254+
studioUrl = studioUrl ?: "",
255+
username = username ?: "",
256+
userHasPassingGrade = userHasPassingGrade ?: false,
257+
verificationData = verificationData?.mapToDomain(),
258+
disableProgressGraph = disableProgressGraph ?: false,
259+
)
260+
}
261+
262+
fun mapToRoomEntity(courseId: String): CourseProgressEntity {
263+
return CourseProgressEntity(
264+
courseId = courseId,
265+
verifiedMode = verifiedMode.orEmpty(),
266+
accessExpiration = accessExpiration.orEmpty(),
267+
certificateData = certificateData?.mapToRoomEntity(),
268+
completionSummary = completionSummary?.mapToRoomEntity(),
269+
courseGrade = courseGrade?.mapToRoomEntity(),
270+
creditCourseRequirements = creditCourseRequirements.orEmpty(),
271+
end = end.orEmpty(),
272+
enrollmentMode = enrollmentMode.orEmpty(),
273+
gradingPolicy = gradingPolicy?.mapToRoomEntity(),
274+
hasScheduledContent = hasScheduledContent ?: false,
275+
sectionScores = sectionScores?.map { it.mapToRoomEntity() } ?: emptyList(),
276+
studioUrl = studioUrl.orEmpty(),
277+
username = username.orEmpty(),
278+
userHasPassingGrade = userHasPassingGrade ?: false,
279+
verificationData = verificationData?.mapToRoomEntity(),
280+
disableProgressGraph = disableProgressGraph ?: false,
281+
)
282+
}
283+
}

0 commit comments

Comments
 (0)