Skip to content

Commit

Permalink
feat: add Course Dates Tab
Browse files Browse the repository at this point in the history
- Add Course dates support.
- Add test cases for Course dates ViewModel.

Not included in this PR:
- Calendar integration
- PLS (shift due dates) banner
- Linking of items on the dates page to specific assessments within the app.

fixes: LEARNER-9664
  • Loading branch information
farhan-arshad-dev committed Nov 8, 2023
1 parent a6fd21c commit 5e6666f
Show file tree
Hide file tree
Showing 28 changed files with 1,409 additions and 42 deletions.
10 changes: 10 additions & 0 deletions app/src/main/java/org/openedx/app/AnalyticsManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,15 @@ class AnalyticsManager(context: Context) : DashboardAnalytics, AuthAnalytics, Ap
)
}

override fun datesTabClickedEvent(courseId: String, courseName: String) {
logEvent(
Event.DATES_TAB_CLICKED, bundleOf(
Key.COURSE_ID.keyName to courseId,
Key.COURSE_NAME.keyName to courseName
)
)
}

override fun handoutsTabClickedEvent(courseId: String, courseName: String) {
logEvent(
Event.HANDOUTS_TAB_CLICKED, bundleOf(
Expand Down Expand Up @@ -402,6 +411,7 @@ private enum class Event(val eventName: String) {
COURSE_TAB_CLICKED("Course_Outline_Course_tab_Clicked"),
VIDEO_TAB_CLICKED("Course_Outline_Videos_tab_Clicked"),
DISCUSSION_TAB_CLICKED("Course_Outline_Discussion_tab_Clicked"),
DATES_TAB_CLICKED("Course_Outline_Dates_tab_Clicked"),
HANDOUTS_TAB_CLICKED("Course_Outline_Handouts_tab_Clicked"),
DISCUSSION_ALL_POSTS_CLICKED("Discussion_All_Posts_Clicked"),
DISCUSSION_FOLLOWING_CLICKED("Discussion_Following_Clicked"),
Expand Down
8 changes: 5 additions & 3 deletions app/src/main/java/org/openedx/app/di/ScreenModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ import org.openedx.core.presentation.dialog.selectorbottomsheet.SelectDialogView
import org.openedx.course.data.repository.CourseRepository
import org.openedx.course.domain.interactor.CourseInteractor
import org.openedx.course.presentation.container.CourseContainerViewModel
import org.openedx.course.presentation.dates.CourseDatesViewModel
import org.openedx.course.presentation.detail.CourseDetailsViewModel
import org.openedx.course.presentation.handouts.HandoutsViewModel
import org.openedx.course.presentation.outline.CourseOutlineViewModel
import org.openedx.course.presentation.section.CourseSectionViewModel
import org.openedx.course.presentation.unit.container.CourseUnitContainerViewModel
import org.openedx.course.presentation.unit.video.EncodedVideoUnitViewModel
import org.openedx.course.presentation.unit.video.VideoUnitViewModel
import org.openedx.course.presentation.unit.video.VideoViewModel
import org.openedx.course.presentation.videos.CourseVideoViewModel
import org.openedx.dashboard.data.repository.DashboardRepository
Expand All @@ -41,14 +44,12 @@ import org.openedx.discussion.presentation.topics.DiscussionTopicsViewModel
import org.openedx.profile.data.repository.ProfileRepository
import org.openedx.profile.domain.interactor.ProfileInteractor
import org.openedx.profile.domain.model.Account
import org.openedx.profile.presentation.anothers_account.AnothersProfileViewModel
import org.openedx.profile.presentation.delete.DeleteProfileViewModel
import org.openedx.profile.presentation.edit.EditProfileViewModel
import org.openedx.profile.presentation.profile.ProfileViewModel
import org.openedx.profile.presentation.settings.video.VideoQualityViewModel
import org.openedx.profile.presentation.settings.video.VideoSettingsViewModel
import org.openedx.course.presentation.unit.video.EncodedVideoUnitViewModel
import org.openedx.course.presentation.unit.video.VideoUnitViewModel
import org.openedx.profile.presentation.anothers_account.AnothersProfileViewModel
import org.openedx.whatsnew.presentation.whatsnew.WhatsNewViewModel

val screenModule = module {
Expand Down Expand Up @@ -91,6 +92,7 @@ val screenModule = module {
viewModel { (courseId: String) -> VideoViewModel(courseId, get(), get(), get()) }
viewModel { (courseId: String) -> VideoUnitViewModel(courseId, get(), get(), get(), get()) }
viewModel { (courseId: String, blockId: String) -> EncodedVideoUnitViewModel(courseId, blockId, get(), get(), get(), get(), get(), get()) }
viewModel { (courseId: String) -> CourseDatesViewModel(courseId, get(), get(), get()) }
viewModel { (courseId:String, handoutsType: String) -> HandoutsViewModel(courseId, handoutsType, get()) }
viewModel { CourseSearchViewModel(get(), get(), get()) }
viewModel { SelectDialogViewModel(get()) }
Expand Down
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ ext {

window_version = '1.1.0'

extented_spans_version = "1.3.0"

//testing
mockk_version = '1.13.3'
android_arch_version = '2.2.0'
Expand Down
5 changes: 4 additions & 1 deletion core/src/main/java/org/openedx/core/data/api/CourseApi.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.openedx.core.data.api

import org.openedx.core.data.model.*
import okhttp3.ResponseBody
import org.openedx.core.data.model.*
import retrofit2.http.*

interface CourseApi {
Expand Down Expand Up @@ -65,6 +65,9 @@ interface CourseApi {
blocksCompletionBody: BlocksCompletionBody
)

@GET("/api/course_home/v1/dates/{course_id}")
suspend fun getCourseDates(@Path("course_id") courseId: String): CourseDates

@GET("/api/mobile/v1/course_info/{course_id}/handouts")
suspend fun getHandouts(@Path("course_id") courseId: String): HandoutsModel

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

import com.google.gson.annotations.SerializedName
import java.util.*

data class CourseDateBlock(
@SerializedName("complete")
val complete: Boolean = false,
@SerializedName("date")
val date: String = "", // ISO 8601 compliant format
@SerializedName("assignment_type")
val assignmentType: String? = "",
@SerializedName("date_type")
val dateType: DateType = DateType.NONE,
@SerializedName("description")
val description: String = "",
@SerializedName("learner_has_access")
val learnerHasAccess: Boolean = false,
@SerializedName("link")
val link: String = "",
@SerializedName("link_text")
val linkText: String = "",
@SerializedName("title")
val title: String = "",
// component blockId in-case of navigating inside the app for component available in mobile
@SerializedName("first_component_block_id")
val blockId: String = "",
)
163 changes: 163 additions & 0 deletions core/src/main/java/org/openedx/core/data/model/CourseDates.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package org.openedx.core.data.model

import com.google.gson.annotations.SerializedName
import org.openedx.core.presentation.course.CourseDatesBadge
import org.openedx.core.utils.TimeUtils
import java.util.Date
import org.openedx.core.domain.model.CourseDateBlock as DomainCourseDateBlock

data class CourseDates(
@SerializedName("dates_banner_info")
val datesBannerInfo: CourseDatesBannerInfo?,
@SerializedName("course_date_blocks")
val courseDateBlocks: List<CourseDateBlock>,
@SerializedName("missed_deadlines")
val missedDeadlines: Boolean = false,
@SerializedName("missed_gated_content")
val missedGatedContent: Boolean = false,
@SerializedName("learner_is_full_access")
val learnerIsFullAccess: Boolean = false,
@SerializedName("user_timezone")
val userTimezone: String? = "",
@SerializedName("verified_upgrade_link")
val verifiedUpgradeLink: String? = "",
) {
fun mapToDomain(): LinkedHashMap<String, ArrayList<DomainCourseDateBlock>> {
var courseDatesDomain = organiseCourseDatesInBlock()
if (isContainToday().not()) {
// Adding today's date block manually if not present in the date
val todayBlock = DomainCourseDateBlock.getTodayDateBlock()
courseDatesDomain[TimeUtils.formatDate(TimeUtils.FORMAT_DATE, todayBlock.date)] =
arrayListOf(todayBlock)
}
// Sort the map entries date keys wise
courseDatesDomain = LinkedHashMap(courseDatesDomain.toSortedMap(compareBy {
TimeUtils.stringToDate(TimeUtils.FORMAT_DATE, it)
}))
reviseDateBlockBadge(courseDatesDomain)
return courseDatesDomain
}

/**
* Map the date blocks according to dates and stack all the blocks of same date against one key
*/
private fun organiseCourseDatesInBlock(): LinkedHashMap<String, ArrayList<DomainCourseDateBlock>> {
val courseDates =
LinkedHashMap<String, ArrayList<DomainCourseDateBlock>>()
courseDateBlocks.forEach { item ->
val key =
TimeUtils.formatDate(TimeUtils.FORMAT_DATE, TimeUtils.iso8601ToDate(item.date))
val dateBlock = DomainCourseDateBlock(
title = item.title,
description = item.description,
link = item.link,
blockId = item.blockId,
date = TimeUtils.iso8601ToDate(item.date),
complete = item.complete,
learnerHasAccess = item.learnerHasAccess,
dateType = item.dateType,
dateBlockBadge = CourseDatesBadge.BLANK
)
if (courseDates.containsKey(key)) {
(courseDates[key] as ArrayList).add(dateBlock)
} else {
courseDates[key] = arrayListOf(dateBlock)
}
}
return courseDates
}

/**
* Utility method to check that list contains today's date block or not.
*/
private fun isContainToday(): Boolean {
val today = Date()
return courseDateBlocks.any { blockDate ->
TimeUtils.iso8601ToDate(blockDate.date) == today
}
}

/**
* Set the Date Block Badge based on the date block data
*/
private fun reviseDateBlockBadge(courseDatesDomain: LinkedHashMap<String, ArrayList<DomainCourseDateBlock>>) {
var dueNextCount = 0
courseDatesDomain.keys.forEach { key ->
courseDatesDomain[key]?.forEach { item ->
var dateBlockTag: CourseDatesBadge = getDateTypeBadge(item)
//Setting Due Next only for first occurrence
if (dateBlockTag == CourseDatesBadge.DUE_NEXT) {
if (dueNextCount == 0)
dueNextCount += 1
else
dateBlockTag = CourseDatesBadge.BLANK
}
item.dateBlockBadge = dateBlockTag
}
}
}

/**
* Return Pill/Badge type of date block based on data
*/
private fun getDateTypeBadge(item: DomainCourseDateBlock): CourseDatesBadge {
val dateBlockTag: CourseDatesBadge
val currentDate = Date()
val componentDate: Date = item.date ?: return CourseDatesBadge.BLANK
when (item.dateType) {
DateType.TODAY_DATE -> {
dateBlockTag = CourseDatesBadge.TODAY
}

DateType.COURSE_START_DATE,
DateType.COURSE_END_DATE -> {
dateBlockTag = CourseDatesBadge.BLANK
}

DateType.ASSIGNMENT_DUE_DATE -> {
when {
item.complete -> {
dateBlockTag = CourseDatesBadge.COMPLETED
}

item.learnerHasAccess -> {
dateBlockTag = when {
item.link.isEmpty() -> {
CourseDatesBadge.NOT_YET_RELEASED
}

TimeUtils.isDueDate(currentDate, componentDate) -> {
CourseDatesBadge.DUE_NEXT
}

TimeUtils.isDatePassed(currentDate, componentDate) -> {
CourseDatesBadge.PAST_DUE
}

else -> {
CourseDatesBadge.BLANK
}
}
}

else -> {
dateBlockTag = CourseDatesBadge.VERIFIED_ONLY
}
}
}

DateType.COURSE_EXPIRED_DATE -> {
dateBlockTag = CourseDatesBadge.COURSE_EXPIRED_DATE
}

else -> {
// dateBlockTag is BLANK for all other cases
// DateTypes.CERTIFICATE_AVAILABLE_DATE,
// DateTypes.VERIFIED_UPGRADE_DEADLINE,
// DateTypes.VERIFICATION_DEADLINE_DATE
dateBlockTag = CourseDatesBadge.BLANK
}
}
return dateBlockTag
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.openedx.core.data.model

import com.google.gson.annotations.SerializedName

data class CourseDatesBannerInfo(
@SerializedName("missed_deadlines")
val missedDeadlines: Boolean = false,
@SerializedName("missed_gated_content")
val missedGatedContent: Boolean = false,
@SerializedName("verified_upgrade_link")
val verifiedUpgradeLink: String = "",
@SerializedName("content_type_gating_enabled")
val contentTypeGatingEnabled: Boolean = false,
) {
fun getCourseBannerType(): CourseBannerType = when {
upgradeToGraded() -> CourseBannerType.UPGRADE_TO_GRADED
upgradeToReset() -> CourseBannerType.UPGRADE_TO_RESET
resetDates() -> CourseBannerType.RESET_DATES
showBannerInfo() -> CourseBannerType.INFO_BANNER
else -> CourseBannerType.BLANK
}

private fun showBannerInfo(): Boolean = missedDeadlines.not()

private fun upgradeToGraded(): Boolean = contentTypeGatingEnabled && missedDeadlines.not()

private fun upgradeToReset(): Boolean =
upgradeToGraded().not() && missedDeadlines && missedGatedContent

private fun resetDates(): Boolean =
upgradeToGraded().not() && missedDeadlines && missedGatedContent.not()
}

enum class CourseBannerType {
BLANK, INFO_BANNER, UPGRADE_TO_GRADED, UPGRADE_TO_RESET, RESET_DATES;
}
31 changes: 31 additions & 0 deletions core/src/main/java/org/openedx/core/data/model/DateType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.openedx.core.data.model

import com.google.gson.annotations.SerializedName

enum class DateType {
@SerializedName("todays-date")
TODAY_DATE,

@SerializedName("course-start-date")
COURSE_START_DATE,

@SerializedName("course-end-date")
COURSE_END_DATE,

@SerializedName("course-expired-date")
COURSE_EXPIRED_DATE,

@SerializedName("assignment-due-date")
ASSIGNMENT_DUE_DATE,

@SerializedName("certificate-available-date")
CERTIFICATE_AVAILABLE_DATE,

@SerializedName("verified-upgrade-deadline")
VERIFIED_UPGRADE_DEADLINE,

@SerializedName("verification-deadline-date")
VERIFICATION_DEADLINE_DATE,

NONE,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.openedx.core.domain.model

import org.openedx.core.data.model.DateType
import org.openedx.core.presentation.course.CourseDatesBadge
import java.util.Date

data class CourseDateBlock(
val title: String = "",
val description: String = "",
val link: String = "",
val blockId: String = "",
val learnerHasAccess: Boolean = false,
val complete: Boolean = false,
val date: Date?,
val dateType: DateType = DateType.NONE,
var dateBlockBadge: CourseDatesBadge = CourseDatesBadge.BLANK,
) {
companion object {
fun getTodayDateBlock() =
CourseDateBlock(
date = Date(),
dateType = DateType.TODAY_DATE,
dateBlockBadge = CourseDatesBadge.TODAY
)
}
}
Loading

0 comments on commit 5e6666f

Please sign in to comment.