Skip to content

Commit

Permalink
feat: added PLS banner for shift dates on Course Dashboard (openedx#211)
Browse files Browse the repository at this point in the history
feat: added PLS banner for shift dates on Course Dashboard

- Banner added on course dashboard if its type is RESET_DATES and course is still available.
- fix failed test cases
- optimise code
- tab position replaced with ordinal
  • Loading branch information
omerhabib26 authored Feb 7, 2024
1 parent 10ae954 commit a4854e3
Show file tree
Hide file tree
Showing 18 changed files with 395 additions and 93 deletions.
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 @@ -71,6 +71,9 @@ interface CourseApi {
@POST("/api/course_experience/v1/reset_course_deadlines")
suspend fun resetCourseDates(@Body courseBody: Map<String, String>): ResetCourseDates

@GET("/api/course_experience/v1/course_deadlines_info/{course_id}")
suspend fun getDatesBannerInfo(@Path("course_id") courseId: String): CourseDatesBannerInfo

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

Expand Down
20 changes: 12 additions & 8 deletions core/src/main/java/org/openedx/core/data/model/CourseDates.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,22 @@ data class CourseDates(
@SerializedName("dates_banner_info")
val datesBannerInfo: DatesBannerInfo?,
@SerializedName("has_ended")
val hasEnded: Boolean,
val hasEnded: Boolean?,
) {
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,
)
courseBanner = getDatesBannerInfo(),
)
}

private fun getDatesBannerInfo(): CourseDatesBannerInfo {
return CourseDatesBannerInfo(
missedDeadlines = datesBannerInfo?.missedDeadlines ?: false,
missedGatedContent = datesBannerInfo?.missedGatedContent ?: false,
verifiedUpgradeLink = datesBannerInfo?.verifiedUpgradeLink ?: "",
contentTypeGatingEnabled = datesBannerInfo?.contentTypeGatingEnabled ?: false,
hasEnded = hasEnded ?: false,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.openedx.core.data.model

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

data class CourseDatesBannerInfo(
@SerializedName("dates_banner_info")
val datesBannerInfo: DatesBannerInfo?,
@SerializedName("has_ended")
val hasEnded: Boolean?,
) {
fun mapToDomain(): CourseDatesBannerInfo {
return CourseDatesBannerInfo(
missedDeadlines = datesBannerInfo?.missedDeadlines ?: false,
missedGatedContent = datesBannerInfo?.missedGatedContent ?: false,
verifiedUpgradeLink = datesBannerInfo?.verifiedUpgradeLink ?: "",
contentTypeGatingEnabled = datesBannerInfo?.contentTypeGatingEnabled ?: false,
hasEnded = hasEnded ?: false,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ data class CourseDatesBannerInfo(
private val missedGatedContent: Boolean,
private val verifiedUpgradeLink: String,
private val contentTypeGatingEnabled: Boolean,
private val hasEnded: Boolean
private val hasEnded: Boolean,
) {
val bannerType by lazy { getCourseBannerType() }

Expand All @@ -25,6 +25,10 @@ data class CourseDatesBannerInfo(
return selfPacedAvailable || instructorPacedAvailable
}

fun isBannerAvailableForDashboard(): Boolean {
return hasEnded.not() && bannerType == RESET_DATES
}

private fun getCourseBannerType(): CourseBannerType = when {
canUpgradeToGraded() -> UPGRADE_TO_GRADED
canUpgradeToReset() -> UPGRADE_TO_RESET
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@
<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_dates_shift_dates_successfully_msg" tools:ignore="MissingTranslation">Your due dates have been successfully shifted to help you stay on track.</string>
<string name="core_dates_shift_dates_unsuccessful_msg" tools:ignore="MissingTranslation">Your dates could not be shifted. Please try again.</string>
<string name="core_dates_view_all_dates" tools:ignore="MissingTranslation"><u>View all dates</u></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 @@ -106,6 +106,9 @@ class CourseRepository(
suspend fun resetCourseDates(courseId: String) =
api.resetCourseDates(mapOf(ApiConstants.COURSE_KEY to courseId)).mapToDomain()

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

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

suspend fun getAnnouncements(courseId: String) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ class CourseInteractor(

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

suspend fun getDatesBannerInfo(courseId: String) = repository.getDatesBannerInfo(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 @@ -2,16 +2,30 @@ package org.openedx.course.presentation.container

import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import org.openedx.course.R

class CourseContainerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {

private val fragments = ArrayList<Fragment>()
private val fragments = HashMap<CourseContainerTab, Fragment>()

override fun getItemCount(): Int = fragments.size

override fun createFragment(position: Int): Fragment = fragments[position]
override fun createFragment(position: Int): Fragment {
val tab = CourseContainerTab.values().find { it.ordinal == position }
return fragments[tab] ?: throw IllegalStateException("Fragment not found for tab $tab")
}

fun addFragment(fragment: Fragment) {
fragments.add(fragment)
fun addFragment(tab: CourseContainerTab, fragment: Fragment) {
fragments[tab] = fragment
}
}

fun getFragment(tab: CourseContainerTab): Fragment? = fragments[tab]
}

enum class CourseContainerTab(val itemId: Int, val titleResId: Int) {
COURSE(itemId = R.id.course, titleResId = R.string.course_navigation_course),
VIDEOS(itemId = R.id.videos, titleResId = R.string.course_navigation_video),
DISCUSSION(itemId = R.id.discussions, titleResId = R.string.course_navigation_discussion),
DATES(itemId = R.id.dates, titleResId = R.string.course_navigation_dates),
HANDOUTS(itemId = R.id.resources, titleResId = R.string.course_navigation_handouts),
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import org.openedx.core.presentation.global.viewBinding
import org.openedx.course.R
import org.openedx.course.databinding.FragmentCourseContainerBinding
import org.openedx.course.presentation.CourseRouter
import org.openedx.course.presentation.container.CourseContainerTab
import org.openedx.course.presentation.dates.CourseDatesFragment
import org.openedx.course.presentation.handouts.HandoutsFragment
import org.openedx.course.presentation.outline.CourseOutlineFragment
import org.openedx.course.presentation.ui.CourseToolbar
import org.openedx.course.presentation.videos.CourseVideosFragment
import org.openedx.discussion.presentation.topics.DiscussionTopicsFragment
import org.openedx.course.presentation.container.CourseContainerTab as Tabs

class CourseContainerFragment : Fragment(R.layout.fragment_course_container) {

Expand Down Expand Up @@ -93,57 +95,45 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) {
binding.viewPager.isVisible = true
binding.viewPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
adapter = CourseContainerAdapter(this).apply {
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.isSelfPaced))
addFragment(HandoutsFragment.newInstance(viewModel.courseId))
addFragment(
Tabs.COURSE,
CourseOutlineFragment.newInstance(viewModel.courseId, viewModel.courseName)
)
addFragment(
Tabs.VIDEOS,
CourseVideosFragment.newInstance(viewModel.courseId, viewModel.courseName)
)
addFragment(
Tabs.DISCUSSION,
DiscussionTopicsFragment.newInstance(viewModel.courseId, viewModel.courseName)
)
addFragment(
Tabs.DATES,
CourseDatesFragment.newInstance(viewModel.courseId, viewModel.isSelfPaced)
)
addFragment(
Tabs.HANDOUTS,
HandoutsFragment.newInstance(viewModel.courseId)
)
}
binding.viewPager.offscreenPageLimit = adapter?.itemCount ?: 1
binding.viewPager.adapter = adapter

if (viewModel.isCourseTopTabBarEnabled) {
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
tab.text = getString(
when (position) {
0 -> R.string.course_navigation_course
1 -> R.string.course_navigation_video
2 -> R.string.course_navigation_discussion
3 -> R.string.course_navigation_dates
else -> R.string.course_navigation_handouts
}
Tabs.values().find { it.ordinal == position }?.titleResId
?: R.string.course_navigation_course
)
}.attach()
binding.tabLayout.isVisible = true

} else {
binding.viewPager.isUserInputEnabled = false
binding.bottomNavView.setOnItemSelectedListener {
when (it.itemId) {
R.id.outline -> {
viewModel.courseTabClickedEvent()
binding.viewPager.setCurrentItem(0, false)
}

R.id.videos -> {
viewModel.videoTabClickedEvent()
binding.viewPager.setCurrentItem(1, false)
}

R.id.discussions -> {
viewModel.discussionTabClickedEvent()
binding.viewPager.setCurrentItem(2, false)
}

R.id.dates -> {
viewModel.datesTabClickedEvent()
binding.viewPager.setCurrentItem(3, false)
}

R.id.resources -> {
viewModel.handoutsTabClickedEvent()
binding.viewPager.setCurrentItem(4, false)
}
binding.bottomNavView.setOnItemSelectedListener { menuItem ->
Tabs.values().find { menuItem.itemId == it.itemId }?.let { tab ->
viewModel.courseContainerTabClickedEvent(tab)
binding.viewPager.setCurrentItem(tab.ordinal, false)
}
true
}
Expand All @@ -155,6 +145,18 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) {
viewModel.updateData(withSwipeRefresh)
}

fun updateCourseDates() {
adapter?.getFragment(Tabs.DATES)?.let {
(it as CourseDatesFragment).updateData()
}
}

fun navigateToTab(tab: CourseContainerTab) {
adapter?.getFragment(tab)?.let {
binding.viewPager.setCurrentItem(tab.ordinal, true)
}
}

companion object {
private const val ARG_COURSE_ID = "courseId"
private const val ARG_TITLE = "title"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,23 +109,33 @@ class CourseContainerViewModel(
}
}

fun courseTabClickedEvent() {
fun courseContainerTabClickedEvent(tab: CourseContainerTab) {
when (tab) {
CourseContainerTab.COURSE -> courseTabClickedEvent()
CourseContainerTab.VIDEOS -> videoTabClickedEvent()
CourseContainerTab.DISCUSSION -> discussionTabClickedEvent()
CourseContainerTab.DATES -> datesTabClickedEvent()
CourseContainerTab.HANDOUTS -> handoutsTabClickedEvent()
}
}

private fun courseTabClickedEvent() {
analytics.courseTabClickedEvent(courseId, courseName)
}

fun videoTabClickedEvent() {
private fun videoTabClickedEvent() {
analytics.videoTabClickedEvent(courseId, courseName)
}

fun discussionTabClickedEvent() {
private fun discussionTabClickedEvent() {
analytics.discussionTabClickedEvent(courseId, courseName)
}

fun datesTabClickedEvent() {
private fun datesTabClickedEvent() {
analytics.datesTabClickedEvent(courseId, courseName)
}

fun handoutsTabClickedEvent() {
private fun handoutsTabClickedEvent() {
analytics.handoutsTabClickedEvent(courseId, courseName)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ import org.openedx.core.utils.TimeUtils
import org.openedx.core.utils.clearTime
import org.openedx.course.R
import org.openedx.course.presentation.CourseRouter
import org.openedx.course.presentation.container.CourseContainerFragment
import org.openedx.course.presentation.ui.CourseDatesBanner
import org.openedx.course.presentation.ui.CourseDatesBannerTablet
import org.openedx.core.R as coreR

class CourseDatesFragment : Fragment() {
Expand Down Expand Up @@ -153,13 +155,22 @@ class CourseDatesFragment : Fragment() {
}
},
onSyncDates = {
viewModel.resetCourseDatesBanner()
viewModel.resetCourseDatesBanner {
if (it) {
(parentFragment as CourseContainerFragment)
.updateCourseStructure(false)
}
}
},
)
}
}
}

fun updateData() {
viewModel.getCourseDates()
}

companion object {
private const val ARG_COURSE_ID = "courseId"
private const val ARG_IS_SELF_PACED = "selfPaced"
Expand Down Expand Up @@ -264,11 +275,19 @@ internal fun CourseDatesScreen(

if (courseBanner.isBannerAvailableForUserType(isSelfPaced)) {
item {
CourseDatesBanner(
modifier = Modifier.padding(bottom = 16.dp),
banner = courseBanner,
resetDates = onSyncDates
)
if (windowSize.isTablet) {
CourseDatesBannerTablet(
modifier = Modifier.padding(bottom = 16.dp),
banner = courseBanner,
resetDates = onSyncDates,
)
} else {
CourseDatesBanner(
modifier = Modifier.padding(bottom = 16.dp),
banner = courseBanner,
resetDates = onSyncDates
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,12 @@ class CourseDatesViewModel(
}
}

fun resetCourseDatesBanner() {
fun resetCourseDatesBanner(onResetDates: (Boolean) -> Unit) {
viewModelScope.launch {
try {
interactor.resetCourseDates(courseId = courseId)
getCourseDates()
onResetDates(true)
} catch (e: Exception) {
if (e.isInternetError()) {
_uiMessage.value =
Expand All @@ -86,6 +87,7 @@ class CourseDatesViewModel(
_uiMessage.value =
UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error))
}
onResetDates(false)
}
}
}
Expand Down
Loading

0 comments on commit a4854e3

Please sign in to comment.