diff --git a/.gitignore b/.gitignore
index 36e2b2501..582c86cb2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,8 @@
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
-xcuserdata/*
+xcuserdata/
+*.xcuserdata/*
/OpenEdX.xcodeproj/xcuserdata/
/OpenEdX.xcworkspace/xcuserdata/
/OpenEdX.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -25,6 +26,15 @@ DerivedData/
*.perspectivev3
!default.perspectivev3
+*.xcodeproj/*
+**/xcuserdata/
+**/*.xcuserdata/*
+!*.xcodeproj/project.pbxproj
+!*.xcodeproj/xcshareddata/
+!*.xcodeproj/project.xcworkspace/
+!*.xcworkspace/contents.xcworkspacedata
+**/xcshareddata/WorkspaceSettings.xcsettings
+
## Obj-C/Swift specific
*.hmap
@@ -100,4 +110,4 @@ iOSInjectionProject/
xcode-frameworks
vendor/
-.bundle/
\ No newline at end of file
+.bundle/
diff --git a/OpenEdX.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Core/Core.xcodeproj.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
similarity index 72%
rename from OpenEdX.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
rename to Core/Core.xcodeproj.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
index 0c67376eb..18d981003 100644
--- a/OpenEdX.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
+++ b/Core/Core.xcodeproj.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -1,5 +1,8 @@
-
+
+ IDEDidComputeMac32BitWarning
+
+
diff --git a/Core/Core.xcodeproj.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Core/Core.xcodeproj.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 000000000..7dde19a07
--- /dev/null
+++ b/Core/Core.xcodeproj.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,14 @@
+{
+ "pins" : [
+ {
+ "identity" : "youtubeplayerkit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/SvenTiigi/YouTubePlayerKit",
+ "state" : {
+ "revision" : "1fe4c8b07a61d50c2fd276e1d9c8087583c7638a",
+ "version" : "1.5.3"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/Core/Core.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Core/Core.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 000000000..919434a62
--- /dev/null
+++ b/Core/Core.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Core/Core.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Core/Core.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 000000000..18d981003
--- /dev/null
+++ b/Core/Core.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/Core/Core.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Core/Core.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 000000000..7dde19a07
--- /dev/null
+++ b/Core/Core.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,14 @@
+{
+ "pins" : [
+ {
+ "identity" : "youtubeplayerkit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/SvenTiigi/YouTubePlayerKit",
+ "state" : {
+ "revision" : "1fe4c8b07a61d50c2fd276e1d9c8087583c7638a",
+ "version" : "1.5.3"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/Core/Core/Extensions/DateExtension.swift b/Core/Core/Extensions/DateExtension.swift
index 059b54cc2..362a3dc33 100644
--- a/Core/Core/Extensions/DateExtension.swift
+++ b/Core/Core/Extensions/DateExtension.swift
@@ -69,6 +69,7 @@ public enum DateStringStyle {
case monthYear
case lastPost
case iso8601
+ case shortWeekdayMonthDayYear
}
public extension Date {
@@ -102,6 +103,8 @@ public extension Date {
dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmDdYyyy
case .iso8601:
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
+ case .shortWeekdayMonthDayYear:
+ dateFormatter.dateFormat = "EEE, MMM d, yyyy"
}
let date = dateFormatter.string(from: self)
@@ -130,6 +133,8 @@ public extension Date {
}
case .iso8601:
return date
+ case .shortWeekdayMonthDayYear:
+ return date
}
}
}
diff --git a/Core/Core/SwiftGen/Strings.swift b/Core/Core/SwiftGen/Strings.swift
index 0197b0494..41a0fcb7d 100644
--- a/Core/Core/SwiftGen/Strings.swift
+++ b/Core/Core/SwiftGen/Strings.swift
@@ -54,6 +54,20 @@ public enum CoreLocalization {
/// Section “
public static let section = CoreLocalization.tr("Localizable", "COURSEWARE.SECTION", fallback: "Section “")
}
+ public enum CourseDates {
+ /// Completed
+ public static let completed = CoreLocalization.tr("Localizable", "COURSE_DATES.COMPLETED", fallback: "Completed")
+ /// Due next
+ public static let dueNext = CoreLocalization.tr("Localizable", "COURSE_DATES.DUE_NEXT", fallback: "Due next")
+ /// Past due
+ public static let pastDue = CoreLocalization.tr("Localizable", "COURSE_DATES.PAST_DUE", fallback: "Past due")
+ /// Today
+ public static let today = CoreLocalization.tr("Localizable", "COURSE_DATES.TODAY", fallback: "Today")
+ /// Unreleased
+ public static let unreleased = CoreLocalization.tr("Localizable", "COURSE_DATES.UNRELEASED", fallback: "Unreleased")
+ /// Verified Only
+ public static let verifiedOnly = CoreLocalization.tr("Localizable", "COURSE_DATES.VERIFIED_ONLY", fallback: "Verified Only")
+ }
public enum Date {
/// Ended
public static let ended = CoreLocalization.tr("Localizable", "DATE.ENDED", fallback: "Ended")
diff --git a/Core/Core/en.lproj/Localizable.strings b/Core/Core/en.lproj/Localizable.strings
index f16f781dc..6c055ab25 100644
--- a/Core/Core/en.lproj/Localizable.strings
+++ b/Core/Core/en.lproj/Localizable.strings
@@ -68,3 +68,10 @@
"WEBVIEW.ALERT.OK" = "Ok";
"WEBVIEW.ALERT.CANCEL" = "Cancel";
+
+"COURSE_DATES.TODAY" = "Today";
+"COURSE_DATES.COMPLETED" = "Completed";
+"COURSE_DATES.PAST_DUE" = "Past due";
+"COURSE_DATES.DUE_NEXT" = "Due next";
+"COURSE_DATES.UNRELEASED" = "Unreleased";
+"COURSE_DATES.VERIFIED_ONLY" = "Verified Only";
diff --git a/Core/Core/uk.lproj/Localizable.strings b/Core/Core/uk.lproj/Localizable.strings
index e06937311..40ff490a3 100644
--- a/Core/Core/uk.lproj/Localizable.strings
+++ b/Core/Core/uk.lproj/Localizable.strings
@@ -68,3 +68,10 @@
"WEBVIEW.ALERT.OK" = "Так";
"WEBVIEW.ALERT.CANCEL" = "Скасувати";
+
+"COURSE_DATES.TODAY" = "Today";
+"COURSE_DATES.COMPLETED" = "Completed";
+"COURSE_DATES.PAST_DUE" = "Past due";
+"COURSE_DATES.DUE_NEXT" = "Due next";
+"COURSE_DATES.UNRELEASED" = "Unreleased";
+"COURSE_DATES.VERIFIED_ONLY" = "Verified Only";
diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj
index c80d63032..16f57b0fb 100644
--- a/Course/Course.xcodeproj/project.pbxproj
+++ b/Course/Course.xcodeproj/project.pbxproj
@@ -64,6 +64,11 @@
0766DFD0299AB29000EBEF6A /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0766DFCF299AB29000EBEF6A /* PlayerViewController.swift */; };
197FB8EA8F92F00A8F383D82 /* Pods_App_Course.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E795BD160CDA7D9C120DE6 /* Pods_App_Course.framework */; };
B8F50317B6B830A0E520C954 /* Pods_App_Course_CourseTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50E59D2B81E12610964282C5 /* Pods_App_Course_CourseTests.framework */; };
+ DB205BFB2AE81B1200136EC2 /* CourseDateViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB205BFA2AE81B1200136EC2 /* CourseDateViewModelTests.swift */; };
+ DB7D6EAC2ADFCAC50036BB13 /* CourseDatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7D6EAB2ADFCAC40036BB13 /* CourseDatesView.swift */; };
+ DB7D6EAE2ADFCB4A0036BB13 /* CourseDatesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7D6EAD2ADFCB4A0036BB13 /* CourseDatesViewModel.swift */; };
+ DB7D6EB02ADFDA0E0036BB13 /* CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7D6EAF2ADFDA0E0036BB13 /* CourseDates.swift */; };
+ DB7D6EB22ADFE9510036BB13 /* Data_CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7D6EB12ADFE9510036BB13 /* Data_CourseDates.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -150,6 +155,11 @@
A47C63D9EB0D866F303D4588 /* Pods-App-Course.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Course/Pods-App-Course.releasestage.xcconfig"; sourceTree = ""; };
ADC2A1B8183A674705F5F7E2 /* Pods-App-Course.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course.debug.xcconfig"; path = "Target Support Files/Pods-App-Course/Pods-App-Course.debug.xcconfig"; sourceTree = ""; };
B196A14555D0E006995A5683 /* Pods-App-CourseDetails.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-CourseDetails.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-CourseDetails/Pods-App-CourseDetails.releaseprod.xcconfig"; sourceTree = ""; };
+ DB205BFA2AE81B1200136EC2 /* CourseDateViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDateViewModelTests.swift; sourceTree = ""; };
+ DB7D6EAB2ADFCAC40036BB13 /* CourseDatesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseDatesView.swift; sourceTree = ""; };
+ DB7D6EAD2ADFCB4A0036BB13 /* CourseDatesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDatesViewModel.swift; sourceTree = ""; };
+ DB7D6EAF2ADFDA0E0036BB13 /* CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDates.swift; sourceTree = ""; };
+ DB7D6EB12ADFE9510036BB13 /* Data_CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_CourseDates.swift; sourceTree = ""; };
DBE05972CB5115D4535C6B8A /* Pods-App-Course-CourseTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course-CourseTests.debug.xcconfig"; path = "Target Support Files/Pods-App-Course-CourseTests/Pods-App-Course-CourseTests.debug.xcconfig"; sourceTree = ""; };
E5E795BD160CDA7D9C120DE6 /* Pods_App_Course.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Course.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E6BDAE887ED8A46860B3F6D3 /* Pods-App-Course-CourseTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course-CourseTests.release.xcconfig"; path = "Target Support Files/Pods-App-Course-CourseTests/Pods-App-Course-CourseTests.release.xcconfig"; sourceTree = ""; };
@@ -291,6 +301,7 @@
027020FB28E7362100F54332 /* Data_CourseOutlineResponse.swift */,
022C64DB29ACFDEE000F532B /* Data_HandoutsResponse.swift */,
022C64DF29ADEA9B000F532B /* Data_UpdatesResponse.swift */,
+ DB7D6EB12ADFE9510036BB13 /* Data_CourseDates.swift */,
);
path = Model;
sourceTree = "";
@@ -319,6 +330,7 @@
02EAE2CA28E1F0A700529644 /* Presentation */ = {
isa = PBXGroup;
children = (
+ DB7D6EAA2ADFCAA00036BB13 /* Dates */,
070019A828F6F33600D5FC78 /* Container */,
070019A628F6F2CB00D5FC78 /* Details */,
070019A728F6F2D600D5FC78 /* Outline */,
@@ -337,6 +349,7 @@
02B6B3C228E1DCD100232911 /* CourseDetails.swift */,
0276D75C29DDA3F80004CDF8 /* ResumeBlock.swift */,
022C64E129ADEB83000F532B /* CourseUpdate.swift */,
+ DB7D6EAF2ADFDA0E0036BB13 /* CourseDates.swift */,
);
path = Model;
sourceTree = "";
@@ -428,6 +441,7 @@
children = (
0262148E29AE17C4008BD75A /* HandoutsViewModelTests.swift */,
0295B1D8297E6DF8003B0C65 /* CourseUnitViewModelTests.swift */,
+ DB205BFA2AE81B1200136EC2 /* CourseDateViewModelTests.swift */,
);
path = Unit;
sourceTree = "";
@@ -462,6 +476,15 @@
path = ../Pods;
sourceTree = "";
};
+ DB7D6EAA2ADFCAA00036BB13 /* Dates */ = {
+ isa = PBXGroup;
+ children = (
+ DB7D6EAB2ADFCAC40036BB13 /* CourseDatesView.swift */,
+ DB7D6EAD2ADFCB4A0036BB13 /* CourseDatesViewModel.swift */,
+ );
+ path = Dates;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -664,6 +687,7 @@
02F78AEB29E6BCA20038DE30 /* VideoPlayerViewModelTests.swift in Sources */,
023812E7297AC8EB0087098F /* CourseDetailsViewModelTests.swift in Sources */,
023812F3297AC9ED0087098F /* CourseMock.generated.swift in Sources */,
+ DB205BFB2AE81B1200136EC2 /* CourseDateViewModelTests.swift in Sources */,
022EA8CB297AD63B0014A8F7 /* CourseContainerViewModelTests.swift in Sources */,
0262148F29AE17C4008BD75A /* HandoutsViewModelTests.swift in Sources */,
);
@@ -685,6 +709,7 @@
02B6B3BC28E1D14F00232911 /* CourseRepository.swift in Sources */,
02280F60294B50030032823A /* CoursePersistenceProtocol.swift in Sources */,
02454CAA2A2619B40043052A /* LessonProgressView.swift in Sources */,
+ DB7D6EB22ADFE9510036BB13 /* Data_CourseDates.swift in Sources */,
02280F5E294B4FDA0032823A /* CourseCoreModel.xcdatamodeld in Sources */,
0766DFCE299AB26D00EBEF6A /* EncodedVideoPlayer.swift in Sources */,
0276D75B29DDA3890004CDF8 /* Data_ResumeBlock.swift in Sources */,
@@ -700,8 +725,10 @@
02A8076829474831007F53AB /* CourseVerticalView.swift in Sources */,
0231124D28EDA804002588FB /* CourseUnitView.swift in Sources */,
027020FC28E7362100F54332 /* Data_CourseOutlineResponse.swift in Sources */,
+ DB7D6EB02ADFDA0E0036BB13 /* CourseDates.swift in Sources */,
02E685C028E4B629000AE015 /* CourseDetailsViewModel.swift in Sources */,
0295C889299BBE8200ABE571 /* CourseNavigationView.swift in Sources */,
+ DB7D6EAE2ADFCB4A0036BB13 /* CourseDatesViewModel.swift in Sources */,
02F066E829DC71750073E13B /* SubtittlesView.swift in Sources */,
022C64E229ADEB83000F532B /* CourseUpdate.swift in Sources */,
02454CA62A26196C0043052A /* UnknownView.swift in Sources */,
@@ -711,6 +738,7 @@
02454CA82A2619890043052A /* DiscussionView.swift in Sources */,
0265B4B728E2141D00E6EAFD /* Strings.swift in Sources */,
02B6B3C128E1DBA100232911 /* Data_CourseDetailsResponse.swift in Sources */,
+ DB7D6EAC2ADFCAC50036BB13 /* CourseDatesView.swift in Sources */,
0766DFCC299AA7A600EBEF6A /* YouTubeVideoPlayer.swift in Sources */,
022F8E162A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift in Sources */,
02E685BE28E4B60A000AE015 /* CourseDetailsView.swift in Sources */,
diff --git a/Course/Course.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Course/Course.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 000000000..919434a62
--- /dev/null
+++ b/Course/Course.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Course/Course.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Course/Course.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 000000000..18d981003
--- /dev/null
+++ b/Course/Course.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/Course/Course/Data/CourseRepository.swift b/Course/Course/Data/CourseRepository.swift
index 68e833255..80ec3c3ef 100644
--- a/Course/Course/Data/CourseRepository.swift
+++ b/Course/Course/Data/CourseRepository.swift
@@ -19,6 +19,8 @@ public protocol CourseRepositoryProtocol {
func getUpdates(courseID: String) async throws -> [CourseUpdate]
func resumeBlock(courseID: String) async throws -> ResumeBlock
func getSubtitles(url: String, selectedLanguage: String) async throws -> String
+ func getCourseDates(courseID: String) async throws -> CourseDates
+ func getCourseDatesOffline(courseID: String) async throws -> CourseDates
}
public class CourseRepository: CourseRepositoryProtocol {
@@ -112,6 +114,18 @@ public class CourseRepository: CourseRepositoryProtocol {
}
}
+ public func getCourseDates(courseID: String) async throws -> CourseDates {
+ let courseDates = try await api.requestData(
+ CourseEndpoint.getCourseDates(courseID: courseID)
+ ).mapResponse(DataLayer.CourseDates.self).domain
+ persistence.saveCourseDates(courseID: courseID, courseDates: courseDates)
+ return courseDates
+ }
+
+ public func getCourseDatesOffline(courseID: String) async throws -> CourseDates {
+ return try persistence.loadCourseDates(courseID: courseID)
+ }
+
private func parseCourseStructure(course: DataLayer.CourseStructure) -> CourseStructure {
let blocks = Array(course.dict.values)
let courseBlock = blocks.first(where: {$0.type == BlockType.course.rawValue })!
@@ -220,6 +234,10 @@ public class CourseRepository: CourseRepositoryProtocol {
// swiftlint:disable all
#if DEBUG
class CourseRepositoryMock: CourseRepositoryProtocol {
+ func getCourseDatesOffline(courseID: String) async throws -> CourseDates {
+ throw NoCachedDataError()
+ }
+
func resumeBlock(courseID: String) async throws -> ResumeBlock {
ResumeBlock(blockID: "123")
}
@@ -232,6 +250,14 @@ class CourseRepositoryMock: CourseRepositoryProtocol {
return [CourseUpdate(id: 1, date: "Date", content: "content", status: "status")]
}
+ func getCourseDates(courseID: String) async throws -> CourseDates {
+ do {
+ let courseDates = try courseDatesJSON.data(using: .utf8)!.mapResponse(DataLayer.CourseDates.self)
+ return courseDates.domain
+ } catch {
+ throw error
+ }
+ }
func getCourseDetailsOffline(courseID: String) async throws -> CourseDetails {
return CourseDetails(
@@ -1034,6 +1060,373 @@ And there are various ways of describing it-- call it oral poetry or
"is_self_paced": false
}
"""
+
+ private let courseDatesJSON: String = """
+ {
+ "dates_banner_info": {
+ "missed_deadlines": false,
+ "content_type_gating_enabled": true,
+ "missed_gated_content": false,
+ "verified_upgrade_link": null
+ },
+ "course_date_blocks": [
+ {
+ "assignment_type": null,
+ "complete": null,
+ "date": "2023-08-30T15:00:00Z",
+ "date_type": "course-start-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "",
+ "link_text": "",
+ "title": "Course starts",
+ "extra_info": null,
+ "first_component_block_id": ""
+ },
+ {
+ "assignment_type": "Problem Set",
+ "complete": false,
+ "date": "2023-09-14T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@ca19e125470846f2a36ad1225410e39a",
+ "link_text": "",
+ "title": "Problem Set 1",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@problem+block@bd89c1dd129240f99bb8c5cbe3f56530"
+ },
+ {
+ "assignment_type": "Problem Set",
+ "complete": false,
+ "date": "2023-09-14T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@ca19e125470846f2a36ad1225410e39aa",
+ "link_text": "",
+ "title": "Problem Set 1.1",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@problem+block@bd89c1dd129240f99bb8c5cbe3f56530a"
+ },
+ {
+ "assignment_type": "Problem Set",
+ "complete": false,
+ "date": "2023-09-21T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@e137765987514da7851a59dedeb5ecec",
+ "link_text": "",
+ "title": "Problem Set 2",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@html+block@c99e81ffff4546e28fecab0a0c381abd"
+ },
+ {
+ "assignment_type": "Problem Set",
+ "complete": true,
+ "date": "2023-09-21T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@e137765987514da7851a59dedeb5ececc",
+ "link_text": "",
+ "title": "Problem Set 2.1",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@html+block@c99e81ffff4546e28fecab0a0c381abdc"
+ },
+ {
+ "assignment_type": "Problem Set",
+ "complete": false,
+ "date": "2023-09-21T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": false,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@e137765987514da7851a59dedeb5ececcc",
+ "link_text": "",
+ "title": "Problem Set 2.2",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@html+block@c99e81ffff4546e28fecab0a0c381abdcc"
+ },
+ {
+ "assignment_type": "Problem Set",
+ "complete": false,
+ "date": "2023-09-28T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@bfe9eb02884a4812883ff9e543887968",
+ "link_text": "",
+ "title": "Problem Set 3",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@html+block@5e117d71433647eaa6de63434641c011"
+ },
+ {
+ "assignment_type": "Midterm",
+ "complete": false,
+ "date": "2023-10-04T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": false,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@bb284b9c4ff04091951f77b50e3b72f4",
+ "link_text": "",
+ "title": "Midterm Exam (time limit removed due to grader issues)",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@vertical+block@ec1c5d83de6244d38b1f3ff4d32b6e17"
+ },
+ {
+ "assignment_type": "Problem Set",
+ "complete": false,
+ "date": "2023-10-12T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@64f4d344ecdc48d2bef514882e6236ab",
+ "link_text": "",
+ "title": "Problem Set 4",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@html+block@eeb64a67e52e4f3e80656b9233204f25"
+ },
+ {
+ "assignment_type": "Problem Set",
+ "complete": false,
+ "date": "2023-10-19T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@79d22d4ab4f740158930fca4e80d67db",
+ "link_text": "",
+ "title": "Problem Set 5",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@html+block@3dde572871fc4b6ebdb47722a184a514"
+ },
+ {
+ "assignment_type": "Problem Set",
+ "complete": false,
+ "date": "2023-10-26T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@3d419098708e4bcd9209ffa31a4cb3dc",
+ "link_text": "",
+ "title": "Problem Set 6",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@problem+block@9b2a0176bf6a4c21ad4a63c2fce2d0cb"
+ },
+ {
+ "assignment_type": "Final Exam",
+ "complete": false,
+ "date": "2023-10-31T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": false,
+ "link": "",
+ "link_text": "",
+ "title": "Final Exam (time limit removed due to grader issues)",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@vertical+block@e7b4f091d7ad457097d0bbda9d9af267"
+ },
+ {
+ "assignment_type": "Finger Exercises",
+ "complete": false,
+ "date": "2023-11-01T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@221a4c17dba341d6a970a0d80343253c",
+ "link_text": "",
+ "title": "1. Introduction to Python (TIME: 1:03:12)",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@ad9387910b7e47069c452efebd7b36dd"
+ },
+ {
+ "assignment_type": "Finger Exercises",
+ "complete": false,
+ "date": "2023-11-01T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@35f82f6c3ecb4e9e913dc279a9b73a9f",
+ "link_text": "",
+ "title": "2. Core Elements of Programs (TIME: 54:14)",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@8fb4fa767a204d41a6366c2bc53bea22"
+ },
+ {
+ "assignment_type": "Finger Exercises",
+ "complete": false,
+ "date": "2023-11-01T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@62f08cc899344863a1ab678aee506dec",
+ "link_text": "",
+ "title": "3. Simple Algorithms (TIME: 41:06)",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@1f2b055948c9467492649b59e24e8fdc"
+ },
+ {
+ "assignment_type": "Finger Exercises",
+ "complete": false,
+ "date": "2023-11-01T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@38007cdb67c44b46b124cdbce33510b5",
+ "link_text": "",
+ "title": "4. Functions (TIME: 1:08:06)",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@9dc4c11c46274b87964c7534b449d50a"
+ },
+ {
+ "assignment_type": "Finger Exercises",
+ "complete": false,
+ "date": "2023-11-01T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@01df98c1e74a459b8fb20d2d785622cd",
+ "link_text": "",
+ "title": "5. Tuples and Lists",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@3464df78190b43948ba0507ef4287290"
+ },
+ {
+ "assignment_type": "Finger Exercises",
+ "complete": false,
+ "date": "2023-11-01T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@8a590293a22e46dd9760ec917d122ec1",
+ "link_text": "",
+ "title": "6. Dictionaries",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@d2abc5b3db0d43ba90c5d3a25e95e2d5"
+ },
+ {
+ "assignment_type": "Finger Exercises",
+ "complete": false,
+ "date": "2023-11-01T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@78648402e8bf4738ade97101cc1ba263",
+ "link_text": "",
+ "title": "7. Testing and Debugging",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@dd0621fbfe594e789b187a1e4f8406eb"
+ },
+ {
+ "assignment_type": "Finger Exercises",
+ "complete": false,
+ "date": "2023-11-01T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@c81c3de20ec54c37a04a8b3d1806e82c",
+ "link_text": "",
+ "title": "8. Exceptions and Assertions",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@6038a1b2f8a340eb8cdb41c021d62234"
+ },
+ {
+ "assignment_type": "Finger Exercises",
+ "complete": false,
+ "date": "2023-11-01T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@37cb9a5012e443bbaa776a80afd9c87a",
+ "link_text": "",
+ "title": "9. Classes and Inheritance",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@b87e596b827142f09e9664fac3ab0be0"
+ },
+ {
+ "assignment_type": "Finger Exercises",
+ "complete": false,
+ "date": "2023-11-01T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@54cd6b1bbbbe40f294ac0b5664c03f1e",
+ "link_text": "",
+ "title": "10. An Extended Example",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@6bc79b1a29ac46a7857caa53a8e203d0"
+ },
+ {
+ "assignment_type": "Finger Exercises",
+ "complete": false,
+ "date": "2023-11-01T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@1334ab336b1b4458b5c2972c50e903b2",
+ "link_text": "",
+ "title": "11. Computational Complexity",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@be73e5a3ee7847d98805a257189b9fad"
+ },
+ {
+ "assignment_type": "Finger Exercises",
+ "complete": false,
+ "date": "2023-11-01T23:30:00Z",
+ "date_type": "assignment-due-date",
+ "description": "",
+ "learner_has_access": true,
+ "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@a7387dbd3728491c8f834e29a73e0cf4",
+ "link_text": "",
+ "title": "12. Searching and Sorting Algorithms",
+ "extra_info": null,
+ "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@fa7e29b3b95b4a3b963d1c5dfdd4e8f8"
+ },
+ {
+ "assignment_type": null,
+ "complete": null,
+ "date": "2023-11-01T23:30:00Z",
+ "date_type": "course-end-date",
+ "description": "After the course ends, the course content will be archived and no longer active.",
+ "learner_has_access": true,
+ "link": "",
+ "link_text": "",
+ "title": "Course ends",
+ "extra_info": null,
+ "first_component_block_id": ""
+ },
+ {
+ "assignment_type": null,
+ "complete": null,
+ "date": "2023-11-03T00:00:00Z",
+ "date_type": "certificate-available-date",
+ "description": "Day certificates will become available for passing verified learners.",
+ "learner_has_access": true,
+ "link": "",
+ "link_text": "",
+ "title": "Certificate Available",
+ "extra_info": null,
+ "first_component_block_id": ""
+ },
+ {
+ "assignment_type": null,
+ "complete": null,
+ "date": "2023-11-23T12:34:28Z",
+ "date_type": "course-expired-date",
+ "description": "You lose all access to this course, including your progress.",
+ "learner_has_access": true,
+ "link": "",
+ "link_text": "",
+ "title": "Audit Access Expires",
+ "extra_info": null,
+ "first_component_block_id": ""
+ }
+ ],
+ "has_ended": false,
+ "learner_is_full_access": false,
+ "user_timezone": null
+ }
+ """
}
#endif
// swiftlint:enable all
diff --git a/Course/Course/Data/Model/Data_CourseDates.swift b/Course/Course/Data/Model/Data_CourseDates.swift
new file mode 100644
index 000000000..b78691020
--- /dev/null
+++ b/Course/Course/Data/Model/Data_CourseDates.swift
@@ -0,0 +1,90 @@
+//
+// Data_CourseDates.swift
+// Course
+//
+// Created by Muhammad Umer on 10/18/23.
+//
+
+import Foundation
+import Core
+
+public extension DataLayer {
+ struct CourseDates: Codable {
+ let datesBannerInfo: DatesBannerInfo?
+ let courseDateBlocks: [CourseDateBlock]
+ let hasEnded, learnerIsFullAccess: Bool
+ let userTimezone: String?
+
+ enum CodingKeys: String, CodingKey {
+ case datesBannerInfo = "dates_banner_info"
+ case courseDateBlocks = "course_date_blocks"
+ case hasEnded = "has_ended"
+ case learnerIsFullAccess = "learner_is_full_access"
+ case userTimezone = "user_timezone"
+ }
+ }
+
+ struct CourseDateBlock: Codable {
+ let assignmentType: String?
+ let complete: Bool?
+ let date, dateType, description: String
+ let learnerHasAccess: Bool
+ let link, title: String
+ let linkText: String?
+ let extraInfo: String?
+ let firstComponentBlockID: String
+
+ enum CodingKeys: String, CodingKey {
+ case assignmentType = "assignment_type"
+ case complete, date
+ case dateType = "date_type"
+ case description
+ case learnerHasAccess = "learner_has_access"
+ case link
+ case linkText = "link_text"
+ case title
+ case extraInfo = "extra_info"
+ case firstComponentBlockID = "first_component_block_id"
+ }
+ }
+
+ struct DatesBannerInfo: Codable {
+ let missedDeadlines, contentTypeGatingEnabled, missedGatedContent: Bool
+ let verifiedUpgradeLink: String?
+
+ enum CodingKeys: String, CodingKey {
+ case missedDeadlines = "missed_deadlines"
+ case contentTypeGatingEnabled = "content_type_gating_enabled"
+ case missedGatedContent = "missed_gated_content"
+ case verifiedUpgradeLink = "verified_upgrade_link"
+ }
+ }
+}
+
+public extension DataLayer.CourseDates {
+ var domain: CourseDates {
+ return CourseDates(
+ datesBannerInfo: DatesBannerInfo(
+ missedDeadlines: datesBannerInfo?.missedDeadlines ?? false,
+ contentTypeGatingEnabled: datesBannerInfo?.contentTypeGatingEnabled ?? false,
+ missedGatedContent: datesBannerInfo?.missedGatedContent ?? false,
+ verifiedUpgradeLink: datesBannerInfo?.verifiedUpgradeLink),
+ courseDateBlocks: courseDateBlocks.map { block in
+ CourseDateBlock(
+ assignmentType: block.assignmentType,
+ complete: block.complete,
+ date: Date(iso8601: block.date),
+ dateType: block.dateType,
+ description: block.description,
+ learnerHasAccess: block.learnerHasAccess,
+ link: block.link,
+ linkText: block.linkText ?? nil,
+ title: block.title,
+ extraInfo: block.extraInfo,
+ firstComponentBlockID: block.firstComponentBlockID)
+ },
+ hasEnded: hasEnded,
+ learnerIsFullAccess: learnerIsFullAccess,
+ userTimezone: userTimezone)
+ }
+}
diff --git a/Course/Course/Data/Model/Data_CourseDetailsResponse.swift b/Course/Course/Data/Model/Data_CourseDetailsResponse.swift
index 306ca6f96..1047727e8 100644
--- a/Course/Course/Data/Model/Data_CourseDetailsResponse.swift
+++ b/Course/Course/Data/Model/Data_CourseDetailsResponse.swift
@@ -22,7 +22,7 @@ public extension DataLayer {
public let name: String
public let number: String
public let org: String
- public let shortDescription: String
+ public let shortDescription: String?
public let start: String?
public let startDisplay: String?
public let startType: String?
diff --git a/Course/Course/Data/Network/CourseEndpoint.swift b/Course/Course/Data/Network/CourseEndpoint.swift
index 7b3109a9c..63ef3b4e1 100644
--- a/Course/Course/Data/Network/CourseEndpoint.swift
+++ b/Course/Course/Data/Network/CourseEndpoint.swift
@@ -19,6 +19,7 @@ enum CourseEndpoint: EndPointType {
case getUpdates(courseID: String)
case resumeBlock(userName: String, courseID: String)
case getSubtitles(url: String, selectedLanguage: String)
+ case getCourseDates(courseID: String)
var path: String {
switch self {
@@ -40,6 +41,8 @@ enum CourseEndpoint: EndPointType {
return "/api/mobile/v1/users/\(userName)/course_status_info/\(courseID)"
case let .getSubtitles(url, _):
return url
+ case .getCourseDates(courseID: let courseID):
+ return "/api/course_home/v1/dates/\(courseID)"
}
}
@@ -63,6 +66,8 @@ enum CourseEndpoint: EndPointType {
return .get
case .getSubtitles:
return .get
+ case .getCourseDates:
+ return .get
}
}
@@ -112,11 +117,13 @@ enum CourseEndpoint: EndPointType {
case .resumeBlock:
return .requestParameters(encoding: JSONEncoding.default)
case let .getSubtitles(_, subtitleLanguage):
-// let languageCode = Locale.current.languageCode ?? "en"
+ // let languageCode = Locale.current.languageCode ?? "en"
let params: [String: Any] = [
"lang": subtitleLanguage
]
return .requestParameters(parameters: params, encoding: URLEncoding.queryString)
+ case .getCourseDates:
+ return .requestParameters(encoding: JSONEncoding.default)
}
}
}
diff --git a/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents b/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents
index f83d58906..33202dee9 100644
--- a/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents
+++ b/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents
@@ -1,5 +1,5 @@
-
+
@@ -19,6 +19,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift b/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift
index b17874645..9efb9d435 100644
--- a/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift
+++ b/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift
@@ -17,9 +17,11 @@ public protocol CoursePersistenceProtocol {
func saveCourseStructure(structure: DataLayer.CourseStructure)
func saveSubtitles(url: String, subtitlesString: String)
func loadSubtitles(url: String) -> String?
+ func saveCourseDates(courseID: String, courseDates: CourseDates)
+ func loadCourseDates(courseID: String) throws -> CourseDates
}
public final class CourseBundle {
private init() {}
}
-
\ No newline at end of file
+
diff --git a/Course/Course/Domain/CourseInteractor.swift b/Course/Course/Domain/CourseInteractor.swift
index df58f05a9..872b22bde 100644
--- a/Course/Course/Domain/CourseInteractor.swift
+++ b/Course/Course/Domain/CourseInteractor.swift
@@ -21,6 +21,7 @@ public protocol CourseInteractorProtocol {
func getUpdates(courseID: String) async throws -> [CourseUpdate]
func resumeBlock(courseID: String) async throws -> ResumeBlock
func getSubtitles(url: String, selectedLanguage: String) async throws -> [Subtitle]
+ func getCourseDates(courseID: String) async throws -> CourseDates
}
public class CourseInteractor: CourseInteractorProtocol {
@@ -94,6 +95,10 @@ public class CourseInteractor: CourseInteractorProtocol {
return parseSubtitles(from: result)
}
+ public func getCourseDates(courseID: String) async throws -> CourseDates {
+ return try await repository.getCourseDates(courseID: courseID)
+ }
+
private func filterChapter(chapter: CourseChapter) -> CourseChapter {
var newChilds = [CourseSequential]()
for sequential in chapter.childs {
diff --git a/Course/Course/Domain/Model/CourseDates.swift b/Course/Course/Domain/Model/CourseDates.swift
new file mode 100644
index 000000000..e60265941
--- /dev/null
+++ b/Course/Course/Domain/Model/CourseDates.swift
@@ -0,0 +1,211 @@
+//
+// CourseDates.swift
+// Course
+//
+// Created by Muhammad Umer on 10/18/23.
+//
+
+import Foundation
+import Core
+
+public struct CourseDates {
+ let datesBannerInfo: DatesBannerInfo
+ let courseDateBlocks: [CourseDateBlock]
+ let hasEnded, learnerIsFullAccess: Bool
+ let userTimezone: String?
+
+ var sortedDateToCourseDateBlockDict: [Date: [CourseDateBlock]] {
+ var dateToCourseDateBlockDict: [Date: [CourseDateBlock]] = [:]
+ var hasToday = false
+ let today = Date.today
+
+ for block in courseDateBlocks {
+ let date = block.date
+ if date == today {
+ hasToday = true
+ }
+
+ dateToCourseDateBlockDict[date, default: []].append(block)
+ }
+
+ if !hasToday {
+ let todayBlock = CourseDateBlock(
+ assignmentType: nil,
+ complete: nil,
+ date: today,
+ dateType: "",
+ description: "",
+ learnerHasAccess: true,
+ link: "", linkText: nil,
+ title: CoreLocalization.CourseDates.today,
+ extraInfo: nil,
+ firstComponentBlockID: "uniqueIDForToday")
+ dateToCourseDateBlockDict[today] = [todayBlock]
+ }
+
+ return dateToCourseDateBlockDict
+ }
+}
+
+extension Date {
+ static var today: Date {
+ return Calendar.current.startOfDay(for: Date())
+ }
+
+ static func compare(_ fromDate: Date, to toDate: Date) -> ComparisonResult {
+ if fromDate > toDate {
+ return .orderedDescending
+ } else if fromDate < toDate {
+ return .orderedAscending
+ }
+ return .orderedSame
+ }
+
+ var isInPast: Bool {
+ return Date.compare(self, to: .today) == .orderedAscending
+ }
+
+ var isToday: Bool {
+ return Date.compare(self, to: .today) == .orderedSame
+ }
+
+ var isInFuture: Bool {
+ return Date.compare(self, to: .today) == .orderedDescending
+ }
+}
+
+public struct CourseDateBlock {
+ let assignmentType: String?
+ let complete: Bool?
+ let date: Date
+ let dateType, description: String
+ let learnerHasAccess: Bool
+ let link: String
+ let linkText: String?
+ let title: String
+ let extraInfo: String?
+ let firstComponentBlockID: String
+
+ var blockTitle: String {
+ if isToday {
+ return CoreLocalization.CourseDates.today
+ } else {
+ return blockStatus.title
+ }
+ }
+
+ var isInPast: Bool {
+ return date.isInPast
+ }
+
+ var isToday: Bool {
+ if dateType.isEmpty {
+ return true
+ } else {
+ return date.isToday
+ }
+ }
+
+ var isInFuture: Bool {
+ return date.isInFuture
+ }
+
+ var isAssignment: Bool {
+ return BlockStatus.status(of: dateType) == .assignment
+ }
+
+ var isVerifiedOnly: Bool {
+ return !learnerHasAccess
+ }
+
+ var isComplete: Bool {
+ return complete ?? false
+ }
+
+ var isLearnerAssignment: Bool {
+ return learnerHasAccess && isAssignment
+ }
+
+ var isPastDue: Bool {
+ return !isComplete && (date < .today)
+ }
+
+ var isUnreleased: Bool {
+ return link.isEmpty
+ }
+
+ var canShowLink: Bool {
+ return !isUnreleased && isLearnerAssignment
+ }
+
+ var blockStatus: BlockStatus {
+ if isComplete {
+ return .completed
+ }
+
+ if !learnerHasAccess {
+ return .verifiedOnly
+ }
+
+ if isAssignment {
+ if isInPast {
+ return isUnreleased ? .unreleased : .pastDue
+ } else if isToday || isInFuture {
+ return isUnreleased ? .unreleased : .dueNext
+ }
+ }
+
+ return BlockStatus.status(of: dateType)
+ }
+}
+
+public struct DatesBannerInfo {
+ let missedDeadlines, contentTypeGatingEnabled, missedGatedContent: Bool
+ let verifiedUpgradeLink: String?
+}
+
+public enum BlockStatus {
+ case completed
+ case pastDue
+ case dueNext
+ case unreleased
+ case verifiedOnly
+ case assignment
+ case verifiedUpgradeDeadline
+ case courseExpiredDate
+ case verificationDeadlineDate
+ case certificateAvailbleDate
+ case courseStartDate
+ case courseEndDate
+ case event
+
+ static func status(of type: String) -> BlockStatus {
+ switch type {
+ case "assignment-due-date": return .assignment
+ case "verified-upgrade-deadline": return .verifiedUpgradeDeadline
+ case "course-expired-date": return .courseExpiredDate
+ case "verification-deadline-date": return .verificationDeadlineDate
+ case "certificate-available-date": return .certificateAvailbleDate
+ case "course-start-date": return .courseStartDate
+ case "course-end-date": return .courseEndDate
+ default: return .event
+ }
+ }
+
+ var title: String {
+ switch self {
+ case .completed:
+ return CoreLocalization.CourseDates.completed
+ case .pastDue:
+ return CoreLocalization.CourseDates.pastDue
+ case .dueNext:
+ return CoreLocalization.CourseDates.dueNext
+ case .unreleased:
+ return CoreLocalization.CourseDates.unreleased
+ case .verifiedOnly:
+ return CoreLocalization.CourseDates.verifiedOnly
+ default:
+ return ""
+ }
+ }
+}
diff --git a/Course/Course/Domain/Model/CourseDetails.swift b/Course/Course/Domain/Model/CourseDetails.swift
index 0edb58854..6769aff53 100644
--- a/Course/Course/Domain/Model/CourseDetails.swift
+++ b/Course/Course/Domain/Model/CourseDetails.swift
@@ -11,7 +11,7 @@ public struct CourseDetails {
public let courseID: String
public let org: String
public let courseTitle: String
- public let courseDescription: String
+ public let courseDescription: String?
public let courseStart: Date?
public let courseEnd: Date?
public let enrollmentStart: Date?
@@ -24,7 +24,7 @@ public struct CourseDetails {
public init(courseID: String,
org: String,
courseTitle: String,
- courseDescription: String,
+ courseDescription: String?,
courseStart: Date?,
courseEnd: Date?,
enrollmentStart: Date?,
diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift
index 574f71b81..726cdaeb3 100644
--- a/Course/Course/Presentation/Container/CourseContainerView.swift
+++ b/Course/Course/Presentation/Container/CourseContainerView.swift
@@ -15,6 +15,7 @@ public struct CourseContainerView: View {
enum CourseTab {
case course
case videos
+ case dates
case discussion
case handounds
}
@@ -74,6 +75,15 @@ public struct CourseContainerView: View {
}
.tag(CourseTab.videos)
+ CourseDatesView(courseID: courseID,
+ viewModel: Container.shared.resolve(CourseDatesViewModel.self,
+ argument: courseID)!)
+ .tabItem {
+ Image(systemName: "calendar").renderingMode(.template)
+ Text(CourseLocalization.CourseContainer.dates)
+ }
+ .tag(CourseTab.dates)
+
DiscussionTopicsView(courseID: courseID,
viewModel: Container.shared.resolve(DiscussionTopicsViewModel.self,
argument: title)!,
@@ -122,6 +132,8 @@ public struct CourseContainerView: View {
return DiscussionLocalization.title
case .handounds:
return CourseLocalization.CourseContainer.handouts
+ case .dates:
+ return CourseLocalization.CourseContainer.dates
}
}
}
diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift
index 938c187a0..a3f90b8e9 100644
--- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift
+++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift
@@ -165,6 +165,8 @@ public class CourseContainerViewModel: BaseCourseViewModel {
analytics.courseOutlineCourseTabClicked(courseId: courseId, courseName: courseName)
case .videos:
analytics.courseOutlineVideosTabClicked(courseId: courseId, courseName: courseName)
+ case .dates:
+ analytics.courseOutlineDatesTabClicked(courseId: courseId, courseName: courseName)
case .discussion:
analytics.courseOutlineDiscussionTabClicked(courseId: courseId, courseName: courseName)
case .handounds:
diff --git a/Course/Course/Presentation/CourseAnalytics.swift b/Course/Course/Presentation/CourseAnalytics.swift
index 6ad6e0389..7396438b4 100644
--- a/Course/Course/Presentation/CourseAnalytics.swift
+++ b/Course/Course/Presentation/CourseAnalytics.swift
@@ -22,6 +22,7 @@ public protocol CourseAnalytics {
func finishVerticalBackToOutlineClicked(courseId: String, courseName: String)
func courseOutlineCourseTabClicked(courseId: String, courseName: String)
func courseOutlineVideosTabClicked(courseId: String, courseName: String)
+ func courseOutlineDatesTabClicked(courseId: String, courseName: String)
func courseOutlineDiscussionTabClicked(courseId: String, courseName: String)
func courseOutlineHandoutsTabClicked(courseId: String, courseName: String)
}
@@ -46,6 +47,7 @@ class CourseAnalyticsMock: CourseAnalytics {
public func finishVerticalBackToOutlineClicked(courseId: String, courseName: String) {}
public func courseOutlineCourseTabClicked(courseId: String, courseName: String) {}
public func courseOutlineVideosTabClicked(courseId: String, courseName: String) {}
+ public func courseOutlineDatesTabClicked(courseId: String, courseName: String) {}
public func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) {}
public func courseOutlineHandoutsTabClicked(courseId: String, courseName: String) {}
}
diff --git a/Course/Course/Presentation/Dates/CourseDatesView.swift b/Course/Course/Presentation/Dates/CourseDatesView.swift
new file mode 100644
index 000000000..34774e1f0
--- /dev/null
+++ b/Course/Course/Presentation/Dates/CourseDatesView.swift
@@ -0,0 +1,279 @@
+//
+// CourseDatesView.swift
+// Discussion
+//
+// Created by Muhammad Umer on 10/17/23.
+//
+
+import Foundation
+import SwiftUI
+import Core
+
+public struct CourseDatesView: View {
+
+ private let courseID: String
+
+ @StateObject
+ private var viewModel: CourseDatesViewModel
+
+ public init(
+ courseID: String,
+ viewModel: CourseDatesViewModel
+ ) {
+ self.courseID = courseID
+ self._viewModel = StateObject(wrappedValue: { viewModel }())
+ }
+
+ public var body: some View {
+ ZStack {
+ VStack(alignment: .center) {
+ if viewModel.isShowProgress {
+ HStack(alignment: .center) {
+ ProgressBar(size: 40, lineWidth: 8)
+ .padding(.top, 200)
+ .padding(.horizontal)
+ }
+ } else if let courseDates = viewModel.courseDates, !courseDates.courseDateBlocks.isEmpty {
+ CourseDateListView(viewModel: viewModel, courseDates: courseDates)
+ }
+ }
+ if viewModel.showError {
+ VStack {
+ Spacer()
+ SnackBarView(message: viewModel.errorMessage)
+ }
+ .transition(.move(edge: .bottom))
+ .onAppear {
+ doAfter(Theme.Timeout.snackbarMessageLongTimeout) {
+ viewModel.errorMessage = nil
+ }
+ }
+ }
+ }
+ .onFirstAppear {
+ Task {
+ await viewModel.getCourseDates(courseID: courseID)
+ }
+ }
+ .background(
+ Theme.Colors.background
+ .ignoresSafeArea()
+ )
+
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+}
+
+struct Line: Shape {
+ func path(in rect: CGRect) -> Path {
+ var path = Path()
+ path.move(to: CGPoint(x: rect.midX, y: rect.minY))
+ path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
+ return path
+ }
+}
+
+struct TimeLineView: View {
+ let date: Date
+ let firstDate: Date?
+ let lastDate: Date?
+
+ var body: some View {
+ ZStack(alignment: .top) {
+ if lastDate == date {
+ VStack {
+ Line()
+ .stroke(style: StrokeStyle(lineWidth: 1))
+ .frame(maxHeight: 10.0, alignment: .top)
+ Spacer()
+ }
+ } else if firstDate == date {
+ Line()
+ .stroke(style: StrokeStyle(lineWidth: 1))
+ .frame(maxHeight: .infinity, alignment: .top)
+ .padding(.top, 10)
+ } else {
+ Line()
+ .stroke(style: StrokeStyle(lineWidth: 1))
+ .frame(maxHeight: .infinity, alignment: .top)
+ }
+
+ Circle()
+ .frame(width: date.isToday ? 12 : 8, height: date.isToday ? 12 : 8)
+ .foregroundColor({
+ if date.isToday {
+ return Theme.Colors.warning
+ } else if date.isInPast {
+ return Color.gray
+ } else {
+ return Color.black
+ }
+ }())
+ .overlay(Circle().stroke(Color.black, lineWidth: 1))
+ .padding(.top, 5)
+ }
+ .frame(width: 16)
+ }
+}
+
+struct CourseDateListView: View {
+ @ObservedObject var viewModel: CourseDatesViewModel
+ var courseDates: CourseDates
+
+ var body: some View {
+ VStack {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 0) {
+ ForEach(viewModel.sortedDates, id: \.self) { date in
+ let blocks = courseDates.sortedDateToCourseDateBlockDict[date]!
+
+ HStack(alignment: .center) {
+ TimeLineView(date: date,
+ firstDate: viewModel.sortedDates.first,
+ lastDate: viewModel.sortedDates.last)
+
+ let ignoredStatuses: [BlockStatus] = [.courseStartDate, .courseEndDate]
+ let block = blocks[0]
+ let allHaveSameStatus = blocks
+ .filter { !ignoredStatuses.contains($0.blockStatus) }
+ .allSatisfy { $0.blockStatus == block.blockStatus }
+
+ BlockStatusView(block: block,
+ allHaveSameStatus: allHaveSameStatus,
+ blocks: blocks)
+
+ Spacer()
+ }
+ }
+ }
+ .padding(.horizontal, 16)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+ }
+}
+
+struct BlockStatusView: View {
+ let block: CourseDateBlock
+ let allHaveSameStatus: Bool
+ let blocks: [CourseDateBlock]
+
+ var body: some View {
+ VStack(alignment: .leading) {
+ HStack {
+ Text(block.date.dateToString(style: .shortWeekdayMonthDayYear))
+ .font(.subheadline)
+ .bold()
+
+ if block.isToday {
+ Text(block.blockTitle)
+ .font(.footnote)
+ .foregroundColor(Color.black)
+ .padding(EdgeInsets(top: 2, leading: 6, bottom: 2, trailing: 8))
+ .background(Theme.Colors.warning)
+ .cornerRadius(5)
+ }
+
+ if allHaveSameStatus {
+ let lockImageText = block.isVerifiedOnly ? Text(Image(systemName: "lock.fill")) : Text("")
+ Text("\(lockImageText) \(block.blockTitle)")
+ .font(.footnote)
+ .foregroundColor(determineForegroundColor(for: block.blockStatus))
+ .padding(EdgeInsets(top: 2, leading: 6, bottom: 2, trailing: 8))
+ .background(determineBackgroundColor(for: block.blockStatus))
+ .cornerRadius(5)
+ }
+ }
+
+ ForEach(blocks, id: \.firstComponentBlockID) { block in
+ styleBlock(block: block, allHaveSameStatus: allHaveSameStatus)
+ }
+ }
+ .padding(.vertical, 0)
+ .padding(.leading, 5)
+ .padding(.bottom, 10)
+ }
+
+ private func determineForegroundColor(for status: BlockStatus) -> Color {
+ switch status {
+ case .verifiedOnly: return Color.white
+ case .pastDue: return Color.black
+ case .dueNext: return Color.white
+ default: return Color.white.opacity(0)
+ }
+ }
+
+ private func determineBackgroundColor(for status: BlockStatus) -> Color {
+ switch status {
+ case .verifiedOnly: return Color.black.opacity(0.5)
+ case .pastDue: return Color.gray.opacity(0.4)
+ case .dueNext: return Color.black.opacity(0.5)
+ default: return Color.white.opacity(0)
+ }
+ }
+
+ func styleBlock(block: CourseDateBlock, allHaveSameStatus: Bool) -> some View {
+ var attrString = AttributedString("")
+
+ if let prefix = block.assignmentType, !prefix.isEmpty {
+ attrString += AttributedString("\(prefix): ")
+ }
+
+ attrString += block.canShowLink ? getAttributedUnderlineString(string: block.title) : AttributedString(block.title)
+
+ if !allHaveSameStatus {
+ attrString += " "
+ let (status, foregroundColor, backgroundColor) = getStatusDetails(for: block.blockStatus)
+ attrString += getAttributedString(string: status, forgroundColor: foregroundColor, backgroundColor: backgroundColor)
+ }
+
+ return Text(attrString).padding(.bottom, 2).font(.footnote)
+ }
+
+ func getStatusDetails(for blockStatus: BlockStatus) -> (String, Color, Color) {
+ switch blockStatus {
+ case .verifiedOnly:
+ return (CoreLocalization.CourseDates.verifiedOnly, Color.white, Color.black.opacity(0.5))
+ case .pastDue:
+ return (CoreLocalization.CourseDates.pastDue, Color.black, Color.gray.opacity(0.4))
+ case .dueNext:
+ return (CoreLocalization.CourseDates.dueNext, Color.white, Color.black.opacity(0.5))
+ case .unreleased:
+ return (CoreLocalization.CourseDates.unreleased, Color.white.opacity(0), Color.white.opacity(0))
+ default:
+ return ("", Color.white.opacity(0), Color.white.opacity(0))
+ }
+ }
+
+ func getAttributedUnderlineString(string: String) -> AttributedString {
+ var attributedString = AttributedString(string)
+ attributedString.font = .footnote
+ attributedString.underlineStyle = .single
+ return attributedString
+ }
+
+ func getAttributedString(string: String, forgroundColor: Color, backgroundColor: Color) -> AttributedString {
+ var attributedString = AttributedString(string)
+ attributedString.font = .footnote
+ attributedString.foregroundColor = forgroundColor
+ attributedString.backgroundColor = backgroundColor
+ return attributedString
+ }
+}
+
+#if DEBUG
+struct CourseDatesView_Previews: PreviewProvider {
+ static var previews: some View {
+ let viewModel = CourseDatesViewModel(
+ interactor: CourseInteractor(repository: CourseRepositoryMock()),
+ router: CourseRouterMock(),
+ cssInjector: CSSInjectorMock(),
+ connectivity: Connectivity(),
+ courseID: "")
+
+ CourseDatesView(
+ courseID: "",
+ viewModel: viewModel)
+ }
+}
+#endif
diff --git a/Course/Course/Presentation/Dates/CourseDatesViewModel.swift b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift
new file mode 100644
index 000000000..e60d413d9
--- /dev/null
+++ b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift
@@ -0,0 +1,72 @@
+//
+// CourseDatesViewModel.swift
+// Course
+//
+// Created by Muhammad Umer on 10/18/23.
+//
+
+import Foundation
+import Core
+import SwiftUI
+
+public class CourseDatesViewModel: ObservableObject {
+
+ @Published private(set) var isShowProgress = false
+ @Published var showError: Bool = false
+ @Published var courseDates: CourseDates?
+
+ var errorMessage: String? {
+ didSet {
+ withAnimation {
+ showError = errorMessage != nil
+ }
+ }
+ }
+
+ private let interactor: CourseInteractorProtocol
+ let cssInjector: CSSInjector
+ let router: CourseRouter
+ let connectivity: ConnectivityProtocol
+
+ public init(
+ interactor: CourseInteractorProtocol,
+ router: CourseRouter,
+ cssInjector: CSSInjector,
+ connectivity: ConnectivityProtocol,
+ courseID: String
+ ) {
+ self.interactor = interactor
+ self.router = router
+ self.cssInjector = cssInjector
+ self.connectivity = connectivity
+ }
+
+ var sortedDates: [Date] {
+ courseDates?.sortedDateToCourseDateBlockDict.keys.sorted() ?? []
+ }
+
+ func blocks(for date: Date) -> [CourseDateBlock] {
+ courseDates?.sortedDateToCourseDateBlockDict[date] ?? []
+ }
+
+ @MainActor
+ func getCourseDates(courseID: String) async {
+ isShowProgress = true
+ do {
+ courseDates = try await interactor.getCourseDates(courseID: courseID)
+ if courseDates?.courseDateBlocks == nil {
+ isShowProgress = false
+ errorMessage = CoreLocalization.Error.unknownError
+ return
+ }
+ isShowProgress = false
+ } catch let error {
+ isShowProgress = false
+ if error.isInternetError || error is NoCachedDataError {
+ errorMessage = CoreLocalization.Error.slowOrNoInternetConnection
+ } else {
+ errorMessage = CoreLocalization.Error.unknownError
+ }
+ }
+ }
+}
diff --git a/Course/Course/Presentation/Details/CourseDetailsView.swift b/Course/Course/Presentation/Details/CourseDetailsView.swift
index 2b846d526..5323efc64 100644
--- a/Course/Course/Presentation/Details/CourseDetailsView.swift
+++ b/Course/Course/Presentation/Details/CourseDetailsView.swift
@@ -246,7 +246,7 @@ private struct CourseTitleView: View {
var body: some View {
VStack(alignment: .leading, spacing: 10) {
- Text(courseDetails.courseDescription)
+ Text(courseDetails.courseDescription ?? "")
.font(Theme.Fonts.labelSmall)
.padding(.horizontal, 26)
diff --git a/Course/Course/SwiftGen/Strings.swift b/Course/Course/SwiftGen/Strings.swift
index f5719bf9d..9ec7b8b36 100644
--- a/Course/Course/SwiftGen/Strings.swift
+++ b/Course/Course/SwiftGen/Strings.swift
@@ -41,6 +41,8 @@ public enum CourseLocalization {
public enum CourseContainer {
/// Course
public static let course = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.COURSE", fallback: "Course")
+ /// Dates
+ public static let dates = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.DATES", fallback: "Dates")
/// Discussion
public static let discussion = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.DISCUSSION", fallback: "Discussion")
/// Handouts
diff --git a/Course/Course/en.lproj/Localizable.strings b/Course/Course/en.lproj/Localizable.strings
index 0f5edc88f..a37d426c0 100644
--- a/Course/Course/en.lproj/Localizable.strings
+++ b/Course/Course/en.lproj/Localizable.strings
@@ -37,6 +37,7 @@
"COURSE_CONTAINER.COURSE" = "Course";
"COURSE_CONTAINER.VIDEOS" = "Videos";
+"COURSE_CONTAINER.DATES" = "Dates";
"COURSE_CONTAINER.DISCUSSION" = "Discussion";
"COURSE_CONTAINER.HANDOUTS" = "Handouts";
"COURSE_CONTAINER.HANDOUTS_IN_DEVELOPING" = "Handouts In developing";
diff --git a/Course/Course/uk.lproj/Localizable.strings b/Course/Course/uk.lproj/Localizable.strings
index cedc987f1..4f7ff5f87 100644
--- a/Course/Course/uk.lproj/Localizable.strings
+++ b/Course/Course/uk.lproj/Localizable.strings
@@ -36,6 +36,7 @@
"COURSE_CONTAINER.COURSE" = "Курс";
"COURSE_CONTAINER.VIDEOS" = "Всі відео";
+//"COURSE_CONTAINER.DATES" = "Dates";
"COURSE_CONTAINER.DISCUSSION" = "Дискусії";
"COURSE_CONTAINER.HANDOUTS" = "Матеріали";
"COURSE_CONTAINER.HANDOUTS_IN_DEVELOPING" = "Матеріали в процесі розробки";
diff --git a/Course/CourseTests/CourseMock.generated.swift b/Course/CourseTests/CourseMock.generated.swift
index 122385f57..8c909304b 100644
--- a/Course/CourseTests/CourseMock.generated.swift
+++ b/Course/CourseTests/CourseMock.generated.swift
@@ -1124,6 +1124,12 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock {
perform?(`courseId`, `courseName`)
}
+ open func courseOutlineDatesTabClicked(courseId: String, courseName: String) {
+ addInvocation(.m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`)))
+ let perform = methodPerformValue(.m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void
+ perform?(`courseId`, `courseName`)
+ }
+
open func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) {
addInvocation(.m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`)))
let perform = methodPerformValue(.m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void
@@ -1151,6 +1157,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock {
case m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter)
case m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter)
case m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter)
+ case m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter)
case m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter)
case m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter)
@@ -1247,6 +1254,12 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock {
results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName"))
return Matcher.ComparisonResult(results)
+ case (.m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)):
+ var results: [Matcher.ParameterComparisonResult] = []
+ results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId"))
+ results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName"))
+ return Matcher.ComparisonResult(results)
+
case (.m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)):
var results: [Matcher.ParameterComparisonResult] = []
results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId"))
@@ -1277,6 +1290,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock {
case let .m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue
case let .m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue
case let .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue
+ case let .m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue
case let .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue
case let .m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue
}
@@ -1296,6 +1310,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock {
case .m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName: return ".finishVerticalBackToOutlineClicked(courseId:courseName:)"
case .m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineCourseTabClicked(courseId:courseName:)"
case .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineVideosTabClicked(courseId:courseName:)"
+ case .m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineDatesTabClicked(courseId:courseName:)"
case .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineDiscussionTabClicked(courseId:courseName:)"
case .m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineHandoutsTabClicked(courseId:courseName:)"
}
@@ -1329,6 +1344,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock {
public static func finishVerticalBackToOutlineClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))}
public static func courseOutlineCourseTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))}
public static func courseOutlineVideosTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))}
+ public static func courseOutlineDatesTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))}
public static func courseOutlineDiscussionTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))}
public static func courseOutlineHandoutsTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))}
}
@@ -1376,6 +1392,9 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock {
public static func courseOutlineVideosTabClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform {
return Perform(method: .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform)
}
+ public static func courseOutlineDatesTabClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform {
+ return Perform(method: .m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform)
+ }
public static func courseOutlineDiscussionTabClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform {
return Perform(method: .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform)
}
@@ -1671,6 +1690,22 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock {
return __value
}
+ open func getCourseDates(courseID: String) throws -> CourseDates {
+ addInvocation(.m_getCourseDates__courseID_courseID(Parameter.value(`courseID`)))
+ let perform = methodPerformValue(.m_getCourseDates__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void
+ perform?(`courseID`)
+ var __value: CourseDates
+ do {
+ __value = try methodReturnValue(.m_getCourseDates__courseID_courseID(Parameter.value(`courseID`))).casted()
+ } catch MockError.notStubed {
+ onFatalFailure("Stub return value not specified for getCourseDates(courseID: String). Use given")
+ Failure("Stub return value not specified for getCourseDates(courseID: String). Use given")
+ } catch {
+ throw error
+ }
+ return __value
+ }
+
fileprivate enum MethodType {
case m_getCourseDetails__courseID_courseID(Parameter)
@@ -1684,6 +1719,7 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock {
case m_getUpdates__courseID_courseID(Parameter)
case m_resumeBlock__courseID_courseID(Parameter)
case m_getSubtitles__url_urlselectedLanguage_selectedLanguage(Parameter, Parameter)
+ case m_getCourseDates__courseID_courseID(Parameter)
static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult {
switch (lhs, rhs) {
@@ -1743,6 +1779,11 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock {
results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url"))
results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSelectedlanguage, rhs: rhsSelectedlanguage, with: matcher), lhsSelectedlanguage, rhsSelectedlanguage, "selectedLanguage"))
return Matcher.ComparisonResult(results)
+
+ case (.m_getCourseDates__courseID_courseID(let lhsCourseid), .m_getCourseDates__courseID_courseID(let rhsCourseid)):
+ var results: [Matcher.ParameterComparisonResult] = []
+ results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID"))
+ return Matcher.ComparisonResult(results)
default: return .none
}
}
@@ -1760,6 +1801,7 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock {
case let .m_getUpdates__courseID_courseID(p0): return p0.intValue
case let .m_resumeBlock__courseID_courseID(p0): return p0.intValue
case let .m_getSubtitles__url_urlselectedLanguage_selectedLanguage(p0, p1): return p0.intValue + p1.intValue
+ case let .m_getCourseDates__courseID_courseID(p0): return p0.intValue
}
}
func assertionName() -> String {
@@ -1775,6 +1817,7 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock {
case .m_getUpdates__courseID_courseID: return ".getUpdates(courseID:)"
case .m_resumeBlock__courseID_courseID: return ".resumeBlock(courseID:)"
case .m_getSubtitles__url_urlselectedLanguage_selectedLanguage: return ".getSubtitles(url:selectedLanguage:)"
+ case .m_getCourseDates__courseID_courseID: return ".getCourseDates(courseID:)"
}
}
}
@@ -1818,6 +1861,9 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock {
public static func getSubtitles(url: Parameter, selectedLanguage: Parameter, willReturn: [Subtitle]...) -> MethodStub {
return Given(method: .m_getSubtitles__url_urlselectedLanguage_selectedLanguage(`url`, `selectedLanguage`), products: willReturn.map({ StubProduct.return($0 as Any) }))
}
+ public static func getCourseDates(courseID: Parameter, willReturn: CourseDates...) -> MethodStub {
+ return Given(method: .m_getCourseDates__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) }))
+ }
public static func getCourseVideoBlocks(fullStructure: Parameter, willProduce: (Stubber) -> Void) -> MethodStub {
let willReturn: [CourseStructure] = []
let given: Given = { return Given(method: .m_getCourseVideoBlocks__fullStructure_fullStructure(`fullStructure`), products: willReturn.map({ StubProduct.return($0 as Any) })) }()
@@ -1925,6 +1971,16 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock {
willProduce(stubber)
return given
}
+ public static func getCourseDates(courseID: Parameter, willThrow: Error...) -> MethodStub {
+ return Given(method: .m_getCourseDates__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) }))
+ }
+ public static func getCourseDates(courseID: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub {
+ let willThrow: [Error] = []
+ let given: Given = { return Given(method: .m_getCourseDates__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) }()
+ let stubber = given.stubThrows(for: (CourseDates).self)
+ willProduce(stubber)
+ return given
+ }
}
public struct Verify {
@@ -1941,6 +1997,7 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock {
public static func getUpdates(courseID: Parameter) -> Verify { return Verify(method: .m_getUpdates__courseID_courseID(`courseID`))}
public static func resumeBlock(courseID: Parameter) -> Verify { return Verify(method: .m_resumeBlock__courseID_courseID(`courseID`))}
public static func getSubtitles(url: Parameter, selectedLanguage: Parameter) -> Verify { return Verify(method: .m_getSubtitles__url_urlselectedLanguage_selectedLanguage(`url`, `selectedLanguage`))}
+ public static func getCourseDates(courseID: Parameter) -> Verify { return Verify(method: .m_getCourseDates__courseID_courseID(`courseID`))}
}
public struct Perform {
@@ -1980,6 +2037,9 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock {
public static func getSubtitles(url: Parameter, selectedLanguage: Parameter, perform: @escaping (String, String) -> Void) -> Perform {
return Perform(method: .m_getSubtitles__url_urlselectedLanguage_selectedLanguage(`url`, `selectedLanguage`), performs: perform)
}
+ public static func getCourseDates(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform {
+ return Perform(method: .m_getCourseDates__courseID_courseID(`courseID`), performs: perform)
+ }
}
public func given(_ method: Given) {
diff --git a/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift b/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift
new file mode 100644
index 000000000..0b6f6f9bf
--- /dev/null
+++ b/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift
@@ -0,0 +1,469 @@
+//
+// CourseDateViewModelTests.swift
+// CourseTests
+//
+// Created by Muhammad Umer on 10/24/23.
+//
+
+import SwiftyMocky
+import XCTest
+@testable import Core
+@testable import Course
+import Alamofire
+import SwiftUI
+
+final class CourseDateViewModelTests: XCTestCase {
+ func testGetCourseDatesSuccess() async throws {
+ let interactor = CourseInteractorProtocolMock()
+ let router = CourseRouterMock()
+ let cssInjector = CSSInjectorMock()
+ let connectivity = ConnectivityProtocolMock()
+
+ let courseDates = CourseDates(
+ datesBannerInfo:
+ DatesBannerInfo(
+ missedDeadlines: false,
+ contentTypeGatingEnabled: false,
+ missedGatedContent: false,
+ verifiedUpgradeLink: ""),
+ courseDateBlocks: [],
+ hasEnded: false,
+ learnerIsFullAccess: false,
+ userTimezone: nil)
+
+ Given(interactor, .getCourseDates(courseID: .any, willReturn: courseDates))
+
+ let viewModel = CourseDatesViewModel(
+ interactor: interactor,
+ router: router,
+ cssInjector: cssInjector,
+ connectivity: connectivity,
+ courseID: "1")
+
+ await viewModel.getCourseDates(courseID: "1")
+
+ Verify(interactor, .getCourseDates(courseID: .any))
+
+ XCTAssert((viewModel.courseDates != nil))
+ XCTAssertFalse(viewModel.isShowProgress)
+ XCTAssertNil(viewModel.errorMessage)
+ XCTAssertFalse(viewModel.showError)
+ }
+
+ func testGetCourseDatesUnknownError() async throws {
+ let interactor = CourseInteractorProtocolMock()
+ let router = CourseRouterMock()
+ let cssInjector = CSSInjectorMock()
+ let connectivity = ConnectivityProtocolMock()
+
+ Given(interactor, .getCourseDates(courseID: .any, willThrow: NSError()))
+
+ let viewModel = CourseDatesViewModel(
+ interactor: interactor,
+ router: router,
+ cssInjector: cssInjector,
+ connectivity: connectivity,
+ courseID: "1")
+
+ await viewModel.getCourseDates(courseID: "1")
+
+ Verify(interactor, .getCourseDates(courseID: .any))
+
+ XCTAssertTrue(viewModel.showError)
+ XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError)
+ }
+
+ func testNoInternetConnectionError() async throws {
+ let interactor = CourseInteractorProtocolMock()
+ let router = CourseRouterMock()
+ let cssInjector = CSSInjectorMock()
+ let connectivity = ConnectivityProtocolMock()
+
+ let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet))
+
+ Given(interactor, .getCourseDates(courseID: .any, willThrow: noInternetError))
+
+ let viewModel = CourseDatesViewModel(
+ interactor: interactor,
+ router: router,
+ cssInjector: cssInjector,
+ connectivity: connectivity,
+ courseID: "1")
+
+ await viewModel.getCourseDates(courseID: "1")
+
+ Verify(interactor, .getCourseDates(courseID: .any))
+
+ XCTAssertTrue(viewModel.showError)
+ XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.slowOrNoInternetConnection)
+ }
+
+ func testSortedDateTodayToCourseDateBlockDict() {
+ let block1 = CourseDateBlock(
+ assignmentType: nil,
+ complete: nil,
+ date: Date.today.addingTimeInterval(86400),
+ dateType: "event",
+ description: "",
+ learnerHasAccess: true,
+ link: "www.example.com",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockID1"
+ )
+
+ let block2 = CourseDateBlock(
+ assignmentType: nil,
+ complete: nil,
+ date: Date.today,
+ dateType: "event",
+ description: "",
+ learnerHasAccess: true,
+ link: "www.example.com",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockID1"
+ )
+
+ let courseDates = CourseDates(
+ datesBannerInfo: DatesBannerInfo(
+ missedDeadlines: false,
+ contentTypeGatingEnabled: false,
+ missedGatedContent: false,
+ verifiedUpgradeLink: nil
+ ),
+ courseDateBlocks: [block1, block2],
+ hasEnded: false,
+ learnerIsFullAccess: true,
+ userTimezone: nil
+ )
+
+ let sortedDict = courseDates.sortedDateToCourseDateBlockDict
+
+ XCTAssertEqual(sortedDict.keys.sorted().first, Date.today)
+ }
+
+ func testMultipleBlocksForSameDate() {
+ let block1 = CourseDateBlock(
+ assignmentType: nil,
+ complete: nil,
+ date: Date.today,
+ dateType: "event",
+ description: "",
+ learnerHasAccess: true,
+ link: "www.example.com",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockID1"
+ )
+
+ let block2 = CourseDateBlock(
+ assignmentType: nil,
+ complete: nil,
+ date: Date.today,
+ dateType: "event",
+ description: "",
+ learnerHasAccess: true,
+ link: "www.example.com",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockID1"
+ )
+
+ let courseDates = CourseDates(
+ datesBannerInfo: DatesBannerInfo(
+ missedDeadlines: false,
+ contentTypeGatingEnabled: false,
+ missedGatedContent: false,
+ verifiedUpgradeLink: nil
+ ),
+ courseDateBlocks: [block1, block2],
+ hasEnded: false,
+ learnerIsFullAccess: true,
+ userTimezone: nil
+ )
+
+ let sortedDict = courseDates.sortedDateToCourseDateBlockDict
+ XCTAssertEqual(sortedDict[block1.date]?.count, 2, "There should be two blocks for the given date.")
+ }
+
+ func testBlockStatusForAssignmentType() {
+ let block = CourseDateBlock(
+ assignmentType: nil,
+ complete: nil,
+ date: Date(),
+ dateType: "assignment-due-date",
+ description: "",
+ learnerHasAccess: true,
+ link: "www.example.com",
+ linkText: nil,
+ title: "TestAssignment",
+ extraInfo: nil,
+ firstComponentBlockID: "blockID3"
+ )
+
+ XCTAssertEqual(block.blockStatus, .dueNext)
+ }
+
+ func testBadgeLogicForToday() {
+ let block = CourseDateBlock(
+ assignmentType: nil,
+ complete: false,
+ date: Date.today,
+ dateType: "",
+ description: "",
+ learnerHasAccess: false,
+ link: "www.example.com",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockIDTest"
+ )
+
+ XCTAssertEqual(block.blockTitle, "Today", "Block title for 'today' should be 'Today'")
+ }
+
+ func testBadgeLogicForCompleted() {
+ let block = CourseDateBlock(
+ assignmentType: nil,
+ complete: true,
+ date: Date.today,
+ dateType: "assignment-due-date",
+ description: "",
+ learnerHasAccess: true,
+ link: "www.example.com",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockIDTest"
+ )
+ XCTAssertEqual(block.blockStatus, .completed, "Block status for a completed assignment should be 'completed'")
+ }
+
+ func testBadgeLogicForVerifiedOnly() {
+ let block = CourseDateBlock(
+ assignmentType: nil,
+ complete: false,
+ date: Date.today,
+ dateType: "assignment-due-date",
+ description: "",
+ learnerHasAccess: false,
+ link: "www.example.com",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockIDTest"
+ )
+
+ XCTAssertEqual(block.blockStatus, .verifiedOnly, "Block status for a block without learner access should be 'verifiedOnly'")
+ }
+
+ func testBadgeLogicForPastDue() {
+ let block = CourseDateBlock(
+ assignmentType: nil,
+ complete: false,
+ date: Date.today.addingTimeInterval(-86400),
+ dateType: "assignment-due-date",
+ description: "",
+ learnerHasAccess: true,
+ link: "www.example.com",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockIDTest"
+ )
+
+ XCTAssertEqual(block.blockStatus, .pastDue, "Block status for a past due assignment should be 'pastDue'")
+ }
+
+ func testLinkForAvailableAssignment() {
+ let availableAssignment = CourseDateBlock(
+ assignmentType: nil,
+ complete: true,
+ date: Date.today,
+ dateType: "assignment-due-date",
+ description: "",
+ learnerHasAccess: true,
+ link: "www.example.com",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockIDTest"
+ )
+ XCTAssertTrue(availableAssignment.canShowLink, "Available assignments should be hyperlinked.")
+ }
+
+ func testIsAssignment() {
+ let block = CourseDateBlock(
+ assignmentType: nil,
+ complete: false,
+ date: Date.today.addingTimeInterval(86400),
+ dateType: "assignment-due-date",
+ description: "",
+ learnerHasAccess: false,
+ link: "www.example.com",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockIDTest"
+ )
+
+ XCTAssertTrue(block.isAssignment)
+ }
+
+ func testIsCourseStartDate() {
+ let block = CourseDateBlock(
+ assignmentType: nil,
+ complete: nil,
+ date: Date.today.addingTimeInterval(-86400),
+ dateType: "course-start-date",
+ description: "",
+ learnerHasAccess: true,
+ link: "www.example.com",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockIDTest"
+ )
+
+ XCTAssertEqual(block.blockStatus, BlockStatus.courseStartDate)
+ }
+
+ func testIsCourseEndDate() {
+ let block = CourseDateBlock(
+ assignmentType: nil,
+ complete: nil,
+ date: Date.today.addingTimeInterval(86400),
+ dateType: "course-end-date",
+ description: "",
+ learnerHasAccess: true,
+ link: "www.example.com",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockIDTest"
+ )
+
+ XCTAssertEqual(block.blockStatus, BlockStatus.courseEndDate)
+ }
+
+ func testVerifiedOnly() {
+ let block = CourseDateBlock(
+ assignmentType: nil,
+ complete: false,
+ date: Date.today.addingTimeInterval(86400),
+ dateType: "assignment-due-date",
+ description: "",
+ learnerHasAccess: false,
+ link: "www.example.com",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockIDTest"
+ )
+
+ XCTAssertTrue(block.isVerifiedOnly)
+ }
+
+ func testIsCompleted() {
+ let block = CourseDateBlock(
+ assignmentType: nil,
+ complete: true,
+ date: Date.today,
+ dateType: "assignment-due-date",
+ description: "",
+ learnerHasAccess: false,
+ link: "www.example.com",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockIDTest"
+ )
+
+ XCTAssertTrue(block.isComplete)
+ }
+
+ func testBadgeLogicForUnreleasedAssignment() {
+ let block = CourseDateBlock(
+ assignmentType: nil,
+ complete: false,
+ date: Date.today.addingTimeInterval(86400),
+ dateType: "assignment-due-date",
+ description: "",
+ learnerHasAccess: true,
+ link: "",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockIDTest"
+ )
+
+ XCTAssertEqual(block.blockStatus, .unreleased)
+ }
+
+ func testNoLinkForUnavailableAssignment() {
+ let block = CourseDateBlock(
+ assignmentType: nil,
+ complete: false,
+ date: Date.today.addingTimeInterval(86400),
+ dateType: "assignment-due-date",
+ description: "",
+ learnerHasAccess: false,
+ link: "www.example.com",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockIDTest"
+ )
+
+ XCTAssertFalse(block.canShowLink)
+ }
+
+ func testNoLinkAvailableForUnreleasedAssignment() {
+ let block = CourseDateBlock(
+ assignmentType: nil,
+ complete: false,
+ date: Date.today,
+ dateType: "assignment-due-date",
+ description: "",
+ learnerHasAccess: false,
+ link: "",
+ linkText: nil,
+ title: "TestBlock",
+ extraInfo: nil,
+ firstComponentBlockID: "blockIDTest"
+ )
+
+ XCTAssertFalse(block.canShowLink)
+ }
+
+ func testTodayProperty() {
+ let today = Date.today
+ let currentDay = Calendar.current.startOfDay(for: Date())
+ XCTAssertEqual(today, currentDay, "The today property should equal the start of the current day.")
+ }
+
+ func testDateIsInPastProperty() {
+ let pastDate = Date().addingTimeInterval(-100000)
+ XCTAssertTrue(pastDate.isInPast)
+ }
+
+ func testDateIsInFutureProperty() {
+ let futureDate = Date().addingTimeInterval(100000)
+ XCTAssertTrue(futureDate.isInFuture)
+ }
+
+ func testBlockStatusMapping() {
+ XCTAssertEqual(BlockStatus.status(of: "course-start-date"), .courseStartDate, "Incorrect mapping for 'course-start-date'")
+ XCTAssertEqual(BlockStatus.status(of: "course-end-date"), .courseEndDate, "Incorrect mapping for 'course-end-date'")
+ XCTAssertEqual(BlockStatus.status(of: "certificate-available-date"), .certificateAvailbleDate, "Incorrect mapping for 'certificate-available-date'")
+ XCTAssertEqual(BlockStatus.status(of: "verification-deadline-date"), .verificationDeadlineDate, "Incorrect mapping for 'verification-deadline-date'")
+ XCTAssertEqual(BlockStatus.status(of: "verified-upgrade-deadline"), .verifiedUpgradeDeadline, "Incorrect mapping for 'verified-upgrade-deadline'")
+ XCTAssertEqual(BlockStatus.status(of: "assignment-due-date"), .assignment, "Incorrect mapping for 'assignment-due-date'")
+ XCTAssertEqual(BlockStatus.status(of: ""), .event, "Incorrect mapping for ''")
+ }
+}
diff --git a/Discovery/Discovery.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Discovery/Discovery.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 000000000..919434a62
--- /dev/null
+++ b/Discovery/Discovery.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Discovery/Discovery.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Discovery/Discovery.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 000000000..18d981003
--- /dev/null
+++ b/Discovery/Discovery.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/OpenEdX.xcodeproj/project.xcworkspace/xcuserdata/vchekyrta.xcuserdatad/UserInterfaceState.xcuserstate b/OpenEdX.xcodeproj/project.xcworkspace/xcuserdata/vchekyrta.xcuserdatad/UserInterfaceState.xcuserstate
deleted file mode 100644
index b59556e65..000000000
Binary files a/OpenEdX.xcodeproj/project.xcworkspace/xcuserdata/vchekyrta.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
diff --git a/OpenEdX/AnalyticsManager.swift b/OpenEdX/AnalyticsManager.swift
index 04a11b640..598aac053 100644
--- a/OpenEdX/AnalyticsManager.swift
+++ b/OpenEdX/AnalyticsManager.swift
@@ -257,6 +257,14 @@ class AnalyticsManager: AuthorizationAnalytics,
logEvent(.courseOutlineVideosTabClicked, parameters: parameters)
}
+ public func courseOutlineDatesTabClicked(courseId: String, courseName: String) {
+ let parameters = [
+ Key.courseID: courseId,
+ Key.courseName: courseName
+ ]
+ logEvent(.courseOutlineDatesTabClicked, parameters: parameters)
+ }
+
public func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) {
let parameters = [
Key.courseID: courseId,
@@ -360,6 +368,7 @@ enum Event: String {
case finishVerticalBackToOutlineClicked = "Finish_Vertical_Back_to_outline_Clicked"
case courseOutlineCourseTabClicked = "Course_Outline_Course_tab_Clicked"
case courseOutlineVideosTabClicked = "Course_Outline_Videos_tab_Clicked"
+ case courseOutlineDatesTabClicked = "Course_Outline_Dates_tab_Clicked"
case courseOutlineDiscussionTabClicked = "Course_Outline_Discussion_tab_Clicked"
case courseOutlineHandoutsTabClicked = "Course_Outline_Handouts_tab_Clicked"
diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift
index c5c52df4c..eb4e04396 100644
--- a/OpenEdX/DI/ScreenAssembly.swift
+++ b/OpenEdX/DI/ScreenAssembly.swift
@@ -298,6 +298,15 @@ class ScreenAssembly: Assembly {
)
}
+ container.register(CourseDatesViewModel.self) { r, courseID in
+ CourseDatesViewModel(
+ interactor: r.resolve(CourseInteractorProtocol.self)!,
+ router: r.resolve(CourseRouter.self)!,
+ cssInjector: r.resolve(CSSInjector.self)!,
+ connectivity: r.resolve(ConnectivityProtocol.self)!,
+ courseID: courseID)
+ }
+
// MARK: Discussion
container.register(DiscussionRepositoryProtocol.self) { r in
DiscussionRepository(
diff --git a/OpenEdX/Data/CoursePersistence.swift b/OpenEdX/Data/CoursePersistence.swift
index cd61e5e6e..6c7f5d83e 100644
--- a/OpenEdX/Data/CoursePersistence.swift
+++ b/OpenEdX/Data/CoursePersistence.swift
@@ -215,4 +215,12 @@ public class CoursePersistence: CoursePersistenceProtocol {
}
return nil
}
+
+ public func saveCourseDates(courseID: String, courseDates: CourseDates) {
+
+ }
+
+ public func loadCourseDates(courseID: String) throws -> CourseDates {
+ throw NoCachedDataError()
+ }
}