Skip to content

Commit

Permalink
feat: Course Dates Banner (openedx#197)
Browse files Browse the repository at this point in the history
Fixes: LEARNER-9730
  • Loading branch information
HamzaIsrar12 authored Jan 25, 2024
1 parent 5e7cab0 commit aa832e2
Show file tree
Hide file tree
Showing 22 changed files with 452 additions and 109 deletions.
10 changes: 9 additions & 1 deletion app/src/main/java/org/openedx/app/di/ScreenModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,15 @@ val screenModule = module {
get()
)
}
viewModel { (courseId: String) -> CourseDatesViewModel(courseId, get(), get(), get()) }
viewModel { (courseId: String, isSelfPaced: Boolean) ->
CourseDatesViewModel(
courseId,
isSelfPaced,
get(),
get(),
get()
)
}
viewModel { (courseId: String, handoutsType: String) ->
HandoutsViewModel(
courseId,
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/java/org/openedx/core/ApiConstants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ object ApiConstants {
const val AUTH_TYPE_GOOGLE = "google-oauth2"
const val AUTH_TYPE_FB = "facebook"
const val AUTH_TYPE_MICROSOFT = "azuread-oauth2"

const val COURSE_KEY = "course_key"
}
3 changes: 3 additions & 0 deletions core/src/main/java/org/openedx/core/data/api/CourseApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ interface CourseApi {
@GET("/api/course_home/v1/dates/{course_id}")
suspend fun getCourseDates(@Path("course_id") courseId: String): CourseDates

@POST("/api/course_experience/v1/reset_course_deadlines")
suspend fun resetCourseDates(@Body courseBody: Map<String, String>): ResetCourseDates

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

Expand Down
33 changes: 20 additions & 13 deletions core/src/main/java/org/openedx/core/data/model/CourseDates.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.openedx.core.data.model

import com.google.gson.annotations.SerializedName
import org.openedx.core.domain.model.CourseDatesBannerInfo
import org.openedx.core.domain.model.CourseDatesResult
import org.openedx.core.domain.model.DatesSection
import org.openedx.core.utils.TimeUtils
import org.openedx.core.utils.addDays
Expand All @@ -10,22 +12,27 @@ 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? = "",
@SerializedName("dates_banner_info")
val datesBannerInfo: DatesBannerInfo?,
@SerializedName("has_ended")
val hasEnded: Boolean,
) {
fun getStructuredCourseDates(): LinkedHashMap<DatesSection, List<DomainCourseDateBlock>> {
fun getCourseDatesResult(): CourseDatesResult {
return CourseDatesResult(
datesSection = getStructuredCourseDates(),
courseBanner = CourseDatesBannerInfo(
missedDeadlines = datesBannerInfo?.missedDeadlines ?: false,
missedGatedContent = datesBannerInfo?.missedGatedContent ?: false,
verifiedUpgradeLink = datesBannerInfo?.verifiedUpgradeLink ?: "",
contentTypeGatingEnabled = datesBannerInfo?.contentTypeGatingEnabled ?: false,
hasEnded = hasEnded,
)
)
}

private fun getStructuredCourseDates(): LinkedHashMap<DatesSection, List<DomainCourseDateBlock>> {
val currentDate = Date()
val courseDatesResponse: LinkedHashMap<DatesSection, List<DomainCourseDateBlock>> =
LinkedHashMap()
Expand Down

This file was deleted.

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

import com.google.gson.annotations.SerializedName

data class DatesBannerInfo(
@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,
)
27 changes: 27 additions & 0 deletions core/src/main/java/org/openedx/core/data/model/ResetCourseDates.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.openedx.core.data.model

import com.google.gson.annotations.SerializedName
import org.openedx.core.domain.model.ResetCourseDates

data class ResetCourseDates(
@SerializedName("message")
val message: String = "",
@SerializedName("body")
val body: String = "",
@SerializedName("header")
val header: String = "",
@SerializedName("link")
val link: String = "",
@SerializedName("link_text")
val linkText: String = "",
) {
fun mapToDomain(): ResetCourseDates {
return ResetCourseDates(
message = message,
body = body,
header = header,
link = link,
linkText = linkText,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.openedx.core.domain.model

import org.openedx.core.R
import org.openedx.core.domain.model.CourseBannerType.BLANK
import org.openedx.core.domain.model.CourseBannerType.INFO_BANNER
import org.openedx.core.domain.model.CourseBannerType.RESET_DATES
import org.openedx.core.domain.model.CourseBannerType.UPGRADE_TO_GRADED
import org.openedx.core.domain.model.CourseBannerType.UPGRADE_TO_RESET

data class CourseDatesBannerInfo(
private val missedDeadlines: Boolean,
private val missedGatedContent: Boolean,
private val verifiedUpgradeLink: String,
private val contentTypeGatingEnabled: Boolean,
private val hasEnded: Boolean
) {
val bannerType by lazy { getCourseBannerType() }

fun isBannerAvailableForUserType(isSelfPaced: Boolean): Boolean {
if (hasEnded) return false

val selfPacedAvailable = isSelfPaced && bannerType != BLANK
val instructorPacedAvailable = !isSelfPaced && bannerType == UPGRADE_TO_GRADED

return selfPacedAvailable || instructorPacedAvailable
}

private fun getCourseBannerType(): CourseBannerType = when {
canUpgradeToGraded() -> UPGRADE_TO_GRADED
canUpgradeToReset() -> UPGRADE_TO_RESET
canResetDates() -> RESET_DATES
infoBanner() -> INFO_BANNER
else -> BLANK
}

private fun infoBanner(): Boolean = !missedDeadlines

private fun canUpgradeToGraded(): Boolean = contentTypeGatingEnabled && !missedDeadlines

private fun canUpgradeToReset(): Boolean =
!canUpgradeToGraded() && missedDeadlines && missedGatedContent

private fun canResetDates(): Boolean =
!canUpgradeToGraded() && missedDeadlines && !missedGatedContent
}

enum class CourseBannerType(
val headerResId: Int = 0,
val bodyResId: Int = 0,
val buttonResId: Int = 0
) {
BLANK,
INFO_BANNER(bodyResId = R.string.core_dates_info_banner_body),
UPGRADE_TO_GRADED(bodyResId = R.string.core_dates_upgrade_to_graded_banner_body),
UPGRADE_TO_RESET(bodyResId = R.string.core_dates_upgrade_to_reset_banner_body),
RESET_DATES(
headerResId = R.string.core_dates_reset_dates_banner_header,
bodyResId = R.string.core_dates_reset_dates_banner_body,
buttonResId = R.string.core_dates_reset_dates_banner_button
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.openedx.core.domain.model

data class CourseDatesResult(
val datesSection: LinkedHashMap<DatesSection, List<CourseDateBlock>>,
val courseBanner: CourseDatesBannerInfo,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.openedx.core.domain.model

data class ResetCourseDates(
val message: String,
val body: String,
val header: String,
val link: String,
val linkText: String,
)
5 changes: 5 additions & 0 deletions core/src/main/java/org/openedx/core/extension/IntExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.openedx.core.extension

fun Int.nonZero(): Int? {
return if (this != 0) this else null
}
7 changes: 2 additions & 5 deletions core/src/main/java/org/openedx/core/ui/theme/Shape.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ import androidx.compose.material.Shapes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.geometry.*
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp

data class AppShapes(
Expand All @@ -22,7 +19,7 @@ data class AppShapes(
val cardShape: CornerBasedShape,
val screenBackgroundShapeFull: CornerBasedShape,
val courseImageShape: CornerBasedShape,
val dialogShape: CornerBasedShape
val dialogShape: CornerBasedShape,
)

internal val LocalShapes = staticCompositionLocalOf {
Expand All @@ -39,7 +36,7 @@ internal val LocalShapes = staticCompositionLocalOf {
cardShape = RoundedCornerShape(12.dp),
screenBackgroundShapeFull = RoundedCornerShape(24.dp),
courseImageShape = RoundedCornerShape(8.dp),
dialogShape = RoundedCornerShape(24.dp)
dialogShape = RoundedCornerShape(24.dp),
)
}

Expand Down
8 changes: 8 additions & 0 deletions core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@
<item quantity="other">%d Items Hidden</item>
</plurals>

<!-- Course Dates Banner -->
<string name="core_dates_reset_dates_banner_button" tools:ignore="MissingTranslation">Shift due dates</string>
<string name="core_dates_reset_dates_banner_header" tools:ignore="MissingTranslation">Missed some deadlines?</string>
<string name="core_dates_reset_dates_banner_body" tools:ignore="MissingTranslation">Don\'t worry - shift our suggested schedule to complete past due assignments without losing any progress.</string>
<string name="core_dates_info_banner_body" tools:ignore="MissingTranslation">We built a suggested schedule to help you stay on track. But don’t worry – it’s flexible so you can learn at your own pace. If you happen to fall behind, you’ll be able to adjust the dates to keep yourself on track.</string>
<string name="core_dates_upgrade_to_graded_banner_body" tools:ignore="MissingTranslation">To complete graded assignments as part of this course, you can upgrade today.</string>
<string name="core_dates_upgrade_to_reset_banner_body" tools:ignore="MissingTranslation">You are auditing this course, which means that you are unable to participate in graded assignments. It looks like you missed some important deadlines based on our suggested schedule. To complete graded assignments as part of this course and shift the past due assignments into the future, you can upgrade today.</string>

<string name="core_register">Register</string>
<string name="core_sign_in">Sign in</string>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.openedx.course.data.repository

import kotlinx.coroutines.flow.map
import okhttp3.ResponseBody
import org.openedx.core.ApiConstants
import org.openedx.core.data.api.CourseApi
import org.openedx.core.data.model.BlocksCompletionBody
import org.openedx.core.data.model.EnrollBody
Expand Down Expand Up @@ -100,7 +101,10 @@ class CourseRepository(
}

suspend fun getCourseDates(courseId: String) =
api.getCourseDates(courseId).getStructuredCourseDates()
api.getCourseDates(courseId).getCourseDatesResult()

suspend fun resetCourseDates(courseId: String) =
api.resetCourseDates(mapOf(ApiConstants.COURSE_KEY to courseId)).mapToDomain()

suspend fun getHandouts(courseId: String) = api.getHandouts(courseId).mapToDomain()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ class CourseInteractor(

suspend fun getCourseDates(courseId: String) = repository.getCourseDates(courseId)

suspend fun resetCourseDates(courseId: String) = repository.resetCourseDates(courseId)

suspend fun getHandouts(courseId: String) = repository.getHandouts(courseId)

suspend fun getAnnouncements(courseId: String) = repository.getAnnouncements(courseId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) {
addFragment(CourseOutlineFragment.newInstance(viewModel.courseId, viewModel.courseName))
addFragment(CourseVideosFragment.newInstance(viewModel.courseId, viewModel.courseName))
addFragment(DiscussionTopicsFragment.newInstance(viewModel.courseId, viewModel.courseName))
addFragment(CourseDatesFragment.newInstance(viewModel.courseId, viewModel.courseName))
addFragment(CourseDatesFragment.newInstance(viewModel.courseId, viewModel.isSelfPaced))
addFragment(HandoutsFragment.newInstance(viewModel.courseId))
}
binding.viewPager.offscreenPageLimit = adapter?.itemCount ?: 1
Expand Down Expand Up @@ -170,4 +170,4 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) {
return fragment
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class CourseContainerViewModel(
val showProgress: LiveData<Boolean>
get() = _showProgress

private var _isSelfPaced: Boolean = true
val isSelfPaced: Boolean
get() = _isSelfPaced

init {
viewModelScope.launch {
notifier.notifier.collect { event ->
Expand All @@ -69,6 +73,7 @@ class CourseContainerViewModel(
}
val courseStructure = interactor.getCourseStructureFromCache()
courseName = courseStructure.name
_isSelfPaced = courseStructure.isSelfPaced
_dataReady.value = courseStructure.start?.let { start ->
start < Date()
}
Expand Down Expand Up @@ -123,5 +128,4 @@ class CourseContainerViewModel(
fun handoutsTabClickedEvent() {
analytics.handoutsTabClickedEvent(courseId, courseName)
}

}
}
Loading

0 comments on commit aa832e2

Please sign in to comment.