Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added ability to handle course errors #14

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 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 @@ -5,6 +5,7 @@ import org.openedx.core.data.model.BlocksCompletionBody
import org.openedx.core.data.model.CourseComponentStatus
import org.openedx.core.data.model.CourseDates
import org.openedx.core.data.model.CourseDatesBannerInfo
import org.openedx.core.data.model.CourseEnrollmentDetails
import org.openedx.core.data.model.CourseEnrollments
import org.openedx.core.data.model.CourseStructureModel
import org.openedx.core.data.model.HandoutsModel
Expand Down Expand Up @@ -76,4 +77,9 @@ interface CourseApi {
@Query("status") status: String? = null,
@Query("requested_fields") fields: List<String> = emptyList()
): CourseEnrollments

@GET("/api/mobile/v1/course_info/{course_id}/enrollment_details")
suspend fun getEnrollmentDetails(
@Path("course_id") courseId: String,
): CourseEnrollmentDetails
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
package org.openedx.core.data.model

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As data model and domain model have the same names, so it will be a good approach if we can define the domain model alias.
i.e,

import org.openedx.core.domain.model.CourseAccessDetails as DomainCourseAccessDetails

import org.openedx.core.data.model.room.discovery.CourseAccessDetailsDb
import org.openedx.core.utils.TimeUtils
import org.openedx.core.domain.model.CourseAccessDetails as DomainCourseAccessDetails

data class CourseAccessDetails(
@SerializedName("has_unmet_prerequisites")
val hasUnmetPrerequisites: Boolean,
@SerializedName("is_too_early")
val isTooEarly: Boolean,
@SerializedName("is_staff")
val isStaff: Boolean,
@SerializedName("audit_access_expires")
val auditAccessExpires: String?,
@SerializedName("courseware_access")
var coursewareAccess: CoursewareAccess?,
) {
fun mapToDomain(): DomainCourseAccessDetails =
DomainCourseAccessDetails(
TimeUtils.iso8601ToDate(auditAccessExpires ?: ""),
coursewareAccess?.mapToDomain()
)
fun mapToDomain() = CourseAccessDetails(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    fun mapToDomain() = DomainCourseAccessDetails(
        hasUnmetPrerequisites = hasUnmetPrerequisites,
        isTooEarly = isTooEarly,
        isStaff = isStaff,
        auditAccessExpires = TimeUtils.iso8601ToDate(auditAccessExpires ?: ""),
        coursewareAccess = coursewareAccess?.mapToDomain(),
    )

hasUnmetPrerequisites = hasUnmetPrerequisites,
isTooEarly = isTooEarly,
isStaff = isStaff,
auditAccessExpires = TimeUtils.iso8601ToDate(auditAccessExpires ?: ""),
coursewareAccess = coursewareAccess?.mapToDomain(),
)

fun mapToRoomEntity(): CourseAccessDetailsDb =
CourseAccessDetailsDb(auditAccessExpires, coursewareAccess?.mapToRoomEntity())
CourseAccessDetailsDb(
hasUnmetPrerequisites, isTooEarly, isStaff,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please mention the attributes names as well.

    fun mapToRoomEntity(): CourseAccessDetailsDb =
        CourseAccessDetailsDb(
            hasUnmetPrerequisites = hasUnmetPrerequisites,
            isTooEarly = isTooEarly,
            isStaff = isStaff,
            auditAccessExpires = auditAccessExpires,
            coursewareAccess = coursewareAccess?.mapToRoomEntity()
        )

auditAccessExpires, coursewareAccess?.mapToRoomEntity()
)
}
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
import org.openedx.core.domain.model.CourseEnrollmentDetails

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here


data class CourseEnrollmentDetails(
@SerializedName("id")
val id: String,
@SerializedName("course_updates")
val courseUpdates: String,
@SerializedName("course_handouts")
val courseHandouts: String,
@SerializedName("discussion_url")
val discussionUrl: String,
@SerializedName("course_access_details")
val courseAccessDetails: CourseAccessDetails,
@SerializedName("certificate")
val certificate: Certificate?,
@SerializedName("enrollment_details")
val enrollmentDetails: EnrollmentDetails,
@SerializedName("course_info_overview")
val courseInfoOverview: CourseInfoOverview,
) {
fun mapToDomain(): CourseEnrollmentDetails {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use alias

return CourseEnrollmentDetails(
id = id,
courseUpdates = courseUpdates,
courseHandouts = courseHandouts,
discussionUrl = discussionUrl,
courseAccessDetails = courseAccessDetails.mapToDomain(),
certificate = certificate?.mapToDomain(),
enrollmentDetails = enrollmentDetails.mapToDomain(),
courseInfoOverview = courseInfoOverview.mapToDomain(),
)
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to define a JsonDeserializer here to update SKUs according to IAP implementation.

    class Deserializer(val corePreferences: CorePreferences) :
        JsonDeserializer<CourseEnrollmentDetails> {
        override fun deserialize(
            json: JsonElement?,
            typeOfT: Type?,
            context: JsonDeserializationContext?,
        ): CourseEnrollmentDetails {
            val courseDetails = Gson().fromJson(json, CourseEnrollmentDetails::class.java)
            if (corePreferences.appConfig.iapConfig.productPrefix.isNotEmpty()) {
                courseDetails.courseInfoOverview.courseModes?.forEach { courseModes ->
                    courseModes.setStoreProductSku(corePreferences.appConfig.iapConfig.productPrefix)
                }
            }
            return courseDetails
        }
    }

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

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

import org.openedx.core.utils.TimeUtils

data class CourseInfoOverview(
@SerializedName("name")
val name: String,
@SerializedName("number")
val number: String,
@SerializedName("org")
val org: String,
@SerializedName("start")
val start: String?,
@SerializedName("start_display")
val startDisplay: String,
@SerializedName("start_type")
val startType: String,
@SerializedName("end")
val end: String?,
@SerializedName("is_self_paced")
val isSelfPaced: Boolean,
@SerializedName("media")
var media: Media?,
@SerializedName("course_sharing_utm_parameters")
val courseSharingUtmParameters: CourseSharingUtmParameters,
@SerializedName("course_about")
val courseAbout: String,
@SerializedName("course_modes")
val courseModes: List<CourseMode>,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CourseModes can be null here.

) {
fun mapToDomain() = CourseInfoOverview(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use alias

name = name,
number = number,
org = org,
start = TimeUtils.iso8601ToDate(start ?: ""),
startDisplay = startDisplay,
startType = startType,
end = TimeUtils.iso8601ToDate(end ?: ""),
isSelfPaced = isSelfPaced,
media = media?.mapToDomain(),
courseSharingUtmParameters = courseSharingUtmParameters.mapToDomain(),
courseAbout = courseAbout,
courseModes = courseModes.map { it.mapToDomain() },

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to add a custom property named ProductInfo in domain CourseInfoOverview model for IAP and set it here as

productInfo = courseModes?.find {
            EnrollmentMode.VERIFIED.toString().equals(it.slug, ignoreCase = true)
        }?.takeIf {
            it.androidSku.isNotNullOrEmpty() && it.storeSku.isNotNullOrEmpty()
        }?.run {
            ProductInfo(courseSku = androidSku!!, storeSku = storeSku!!)
        }

)
}
19 changes: 13 additions & 6 deletions core/src/main/java/org/openedx/core/data/model/CourseMode.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.openedx.core.data.model

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

import kotlin.math.ceil

/**
Expand All @@ -10,20 +11,26 @@ import kotlin.math.ceil
data class CourseMode(
@SerializedName("slug")
val slug: String?,

@SerializedName("sku")
val sku: String?,

@SerializedName("android_sku")
val androidSku: String?,

@SerializedName("ios_sku")
val iosSku: String?,
@SerializedName("min_price")
val price: Double?,

val minPrice: Double?,
var storeSku: String?,
) {
fun mapToDomain() = CourseMode(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use alias

slug = slug,
sku = sku,
androidSku = androidSku,
iosSku = iosSku,
minPrice = minPrice,
storeSku = storeSku
)
fun setStoreProductSku(storeProductPrefix: String) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a blank line

val ceilPrice = price
val ceilPrice = minPrice
?.let { ceil(it).toInt() }
?.takeIf { it > 0 }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ data class CourseStructureModel(
ProductInfo(
courseSku = androidSku!!,
storeSku = storeSku!!,
lmsUSDPrice = price ?: 0.0
lmsUSDPrice = minPrice ?: 0.0
)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ data class EnrolledCourse(
ProductInfo(
courseSku = androidSku!!,
storeSku = storeSku!!,
lmsUSDPrice = price ?: 0.0
lmsUSDPrice = minPrice ?: 0.0
)
}
)
Expand Down
25 changes: 11 additions & 14 deletions core/src/main/java/org/openedx/core/data/model/EnrollmentDetails.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,21 @@ import org.openedx.core.domain.model.EnrollmentDetails as DomainEnrollmentDetail
data class EnrollmentDetails(
@SerializedName("created")
var created: String?,

@SerializedName("date")
val date: String?,
@SerializedName("mode")
var mode: String?,

val mode: String,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mode can be null here.

@SerializedName("is_active")
var isActive: Boolean = false,

val isActive: Boolean = false,
@SerializedName("upgrade_deadline")
var upgradeDeadline: String?,
val upgradeDeadline: String?,
) {
fun mapToDomain(): DomainEnrollmentDetails {
return DomainEnrollmentDetails(
created = TimeUtils.iso8601ToDate(created ?: ""),
mode = mode,
isActive = isActive,
upgradeDeadline = TimeUtils.iso8601ToDate(upgradeDeadline ?: ""),
)
}
fun mapToDomain() = DomainEnrollmentDetails(
created = TimeUtils.iso8601ToDate(date ?: ""),
mode = mode,
isActive = isActive,
upgradeDeadline = TimeUtils.iso8601ToDate(upgradeDeadline ?: ""),
)

fun mapToRoomEntity() = EnrollmentDetailsDB(
created = created,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,15 +267,24 @@ data class EnrollmentDetailsDB(
}

data class CourseAccessDetailsDb(
@ColumnInfo("hasUnmetPrerequisites")
val hasUnmetPrerequisites: Boolean,
@ColumnInfo("isTooEarly")
val isTooEarly: Boolean,
@ColumnInfo("isStaff")
val isStaff: Boolean,
@ColumnInfo("auditAccessExpires")
var auditAccessExpires: String?,
@Embedded
val coursewareAccess: CoursewareAccessDb?,
) {
fun mapToDomain(): CourseAccessDetails {
return CourseAccessDetails(
TimeUtils.iso8601ToDate(auditAccessExpires ?: ""),
coursewareAccess?.mapToDomain()
hasUnmetPrerequisites = hasUnmetPrerequisites,
isTooEarly = isTooEarly,
isStaff = isStaff,
auditAccessExpires = TimeUtils.iso8601ToDate(auditAccessExpires ?: ""),
coursewareAccess = coursewareAccess?.mapToDomain()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import java.util.Date

@Parcelize
data class CourseAccessDetails(
val hasUnmetPrerequisites: Boolean,
val isTooEarly: Boolean,
val isStaff: Boolean,
val auditAccessExpires: Date?,
val coursewareAccess: CoursewareAccess?,
) : Parcelable
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.openedx.core.domain.model

import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.util.Date

@Parcelize
data class CourseEnrollmentDetails(
val id: String,
val courseUpdates: String,
val courseHandouts: String,
val discussionUrl: String,
val courseAccessDetails: CourseAccessDetails,
val certificate: Certificate?,
val enrollmentDetails: EnrollmentDetails,
val courseInfoOverview: CourseInfoOverview,
) : Parcelable {
fun isUpgradable(): Boolean {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can define some utility methods here for efficient use.

    val hasAccess: Boolean
        get() = courseAccessDetails.coursewareAccess?.hasAccess ?: false
    
    val isAuditAccessExpired: Boolean
        get() = courseAccessDetails.auditAccessExpires.isNotNull() &&
                Date().after(courseAccessDetails.auditAccessExpires)


    val isUpgradeable: Boolean
        get() = enrollmentDetails.isAuditMode &&
                courseInfoOverview.isStarted &&
                enrollmentDetails.isUpgradeDeadlinePassed.not() &&
                courseInfoOverview.productInfo.isNotNull()

val start = courseInfoOverview.start ?: return false
val upgradeDeadline = enrollmentDetails.upgradeDeadline ?: return false
if (enrollmentDetails.mode != "audit") return false

return start < Date() && getCourseMode() != null && upgradeDeadline > Date()
}

fun getCourseMode(): CourseMode? {
return courseInfoOverview.courseModes
.firstOrNull { it.slug == "verified" && !it.androidSku.isNullOrEmpty() }
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can also define the enum class CourseAccessError here

enum class CourseAccessError {
    NONE, AUDIT_EXPIRED_NOT_UPGRADABLE, AUDIT_EXPIRED_UPGRADABLE, NOT_YET_STARTED, UNKNOWN
}

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

import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.util.Date

@Parcelize
data class CourseInfoOverview(
val name: String,
val number: String,
val org: String,
val start: Date?,
val startDisplay: String,
val startType: String,
val end: Date?,
val isSelfPaced: Boolean,
var media: Media?,
val courseSharingUtmParameters: CourseSharingUtmParameters,
val courseAbout: String,
val courseModes: List<CourseMode>,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

courseModes can be null.
also define productInfo here as well.

    val productInfo: ProductInfo?

) : Parcelable

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can also define a boolean method here

    val isStarted: Boolean
        get() = start?.before(Date()) ?: false

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class CourseMode(
val slug: String?,
val sku: String?,
val androidSku: String?,
val iosSku: String?,
val minPrice: Double?,
var storeSku: String?,
) : Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ data class EnrollmentDetails(
var created: Date?,
var mode: String?,
var isActive: Boolean,
var upgradeDeadline: Date?,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for this change.

var upgradeDeadline: Date?
) : Parcelable {
val isUpgradeDeadlinePassed: Boolean
get() = TimeUtils.isDatePassed(Date(), upgradeDeadline)
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/java/org/openedx/core/utils/TimeUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,11 @@ object TimeUtils {
}
}

fun getCourseAccessFormattedDate(context: Context, date: Date): String {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add doc for this method.

val resourceManager = ResourceManager(context)
return dateToCourseDate(resourceManager, date)
}

/**
* Returns the number of days difference between the given date and the current date.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.openedx.core.data.api.CourseApi
import org.openedx.core.data.model.BlocksCompletionBody
import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.domain.model.CourseComponentStatus
import org.openedx.core.domain.model.CourseEnrollmentDetails
import org.openedx.core.domain.model.CourseStructure
import org.openedx.core.exception.NoCachedDataException
import org.openedx.core.module.db.DownloadDao
Expand Down Expand Up @@ -58,6 +59,10 @@ class CourseRepository(
return courseStructure[courseId]!!
}

suspend fun getEnrollmentDetails(courseId: String): CourseEnrollmentDetails {
return api.getEnrollmentDetails(courseId = courseId).mapToDomain()
}

suspend fun getCourseStatus(courseId: String): CourseComponentStatus {
val username = preferencesManager.user?.username ?: ""
return api.getCourseStatus(username, courseId).mapToDomain()
Expand Down
Loading
Loading