From d9caaa84ffb5358de887644d213c4f74b034acdf Mon Sep 17 00:00:00 2001 From: Muhammad Umer Date: Wed, 25 Oct 2023 15:24:07 +0100 Subject: [PATCH 01/10] feat: course dates --- .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/swiftpm/Package.resolved | 14 + .../UserInterfaceState.xcuserstate | Bin 0 -> 10771 bytes .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/swiftpm/Package.resolved | 14 + .../UserInterfaceState.xcuserstate | Bin 0 -> 12693 bytes Core/Core/Configuration/Config.swift | 2 +- Core/Core/Data/CoreStorage.swift | 8 +- Core/Core/Extensions/DateExtension.swift | 5 + Course/Course.xcodeproj/project.pbxproj | 28 ++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 11709 bytes Course/Course/Data/CourseRepository.swift | 393 ++++++++++++++++++ .../Course/Data/Model/Data_CourseDates.swift | 90 ++++ .../Model/Data_CourseDetailsResponse.swift | 2 +- .../Course/Data/Network/CourseEndpoint.swift | 9 +- .../CourseCoreModel.xcdatamodel/contents | 27 +- .../CoursePersistenceProtocol.swift | 4 +- Course/Course/Domain/CourseInteractor.swift | 5 + Course/Course/Domain/Model/CourseDates.swift | 210 ++++++++++ .../Course/Domain/Model/CourseDetails.swift | 4 +- .../Container/CourseContainerView.swift | 9 + .../Presentation/Dates/CourseDatesView.swift | 279 +++++++++++++ .../Dates/CourseDatesViewModel.swift | 72 ++++ .../Details/CourseDetailsView.swift | 2 +- .../Presentation/Unit/CourseDatesTests.swift | 31 ++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 15709 bytes OpenEdX/DI/ScreenAssembly.swift | 9 + OpenEdX/Data/CoursePersistence.swift | 8 + OpenEdX/Environment.swift | 12 +- Profile/Data/ProfileStorage.swift | 2 +- 35 files changed, 1273 insertions(+), 19 deletions(-) create mode 100644 Core/Core.xcodeproj.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Core/Core.xcodeproj.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Core/Core.xcodeproj.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 Core/Core.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Core/Core.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Core/Core.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Core/Core.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 Course/Course.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Course/Course.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Course/Course.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 Course/Course/Data/Model/Data_CourseDates.swift create mode 100644 Course/Course/Domain/Model/CourseDates.swift create mode 100644 Course/Course/Presentation/Dates/CourseDatesView.swift create mode 100644 Course/Course/Presentation/Dates/CourseDatesViewModel.swift create mode 100644 Course/CourseTests/Presentation/Unit/CourseDatesTests.swift create mode 100644 Discovery/Discovery.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Discovery/Discovery.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Discovery/Discovery.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate diff --git a/Core/Core.xcodeproj.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Core/Core.xcodeproj.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/Core/Core.xcodeproj.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +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.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate b/Core/Core.xcodeproj.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..dfb82c0a5b97876276784a124fd56bdf8b85b22e GIT binary patch literal 10771 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 sIX`rMw=6BSNHaCLn!B|EB5w2Uh8c#sB~S literal 0 HcmV?d00001 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.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate b/Core/Core.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..81b98d607430a7a2b00037a2d008c07b0530118c GIT binary patch literal 12693 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 literal 0 HcmV?d00001 diff --git a/Core/Core/Configuration/Config.swift b/Core/Core/Configuration/Config.swift index 77f5da816..061364531 100644 --- a/Core/Core/Configuration/Config.swift +++ b/Core/Core/Configuration/Config.swift @@ -11,7 +11,7 @@ public class Config { public let baseURL: URL public let oAuthClientId: String - public let tokenType: TokenType = .jwt + public let tokenType: TokenType = .bearer public lazy var termsOfUse: URL? = { URL(string: "\(baseURL.description)/tos") diff --git a/Core/Core/Data/CoreStorage.swift b/Core/Core/Data/CoreStorage.swift index 4ff71e963..749fb4a79 100644 --- a/Core/Core/Data/CoreStorage.swift +++ b/Core/Core/Data/CoreStorage.swift @@ -8,10 +8,10 @@ import Foundation public protocol CoreStorage { - var accessToken: String? {get set} - var refreshToken: String? {get set} - var cookiesDate: String? {get set} - var user: DataLayer.User? {get set} + var accessToken: String? { get set } + var refreshToken: String? { get set } + var cookiesDate: String? { get set } + var user: DataLayer.User? { get set } var userSettings: UserSettings? {get set} func clear() } 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/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index c80d63032..98aeddf53 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 /* CourseDatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB205BFA2AE81B1200136EC2 /* CourseDatesTests.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 /* CourseDatesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDatesTests.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 /* CourseDatesTests.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 /* CourseDatesTests.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.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate b/Course/Course.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..4fe0ac86f3c7c5ade1e146f3c9281fd39d96ee8f GIT binary patch literal 11709 zcmeHtd3;mV_V3>NBn6r@Icb|TNs}~F3rI%_Eg(Z@C_|~V3@y-*Hm7YMO-fEmDO`0| zxdztFMt>F9c`Mvl4d-Ow2&Yt($ zYkk*ut)2Q-pO+71XTOawB8WjUBu6o50E(UCT*3u;Z@^#UbO#%nz3^7;3>MpHHGWoJcZ##htNPYXl7N&9pWh7rokGdKuVN^^vHlRP$tSk*(e7M zN4aPO8j12yJ{pB4qbX=AnuaRSbX1A1LUYkPG#}NWg=i7-p%&yvL9`TgplcCDE70H3 zjc6@ehyI0bL+jB!=w8%=Hlb(HUbGMGN6(?>(F^EB^b&d*y@Fmv2hkho2znbGMem^Z z&}sA)I)lDO-=J^Ncj!0tUvwV*jxp9?Gq&I~oQ__{88`=z#CdoOz7kKs1-KBG;xar5 zSK#Tm3eU!K@I1T#H)1bdh!^1i=5afw_y&9nUWf0%>+!v~2XDfg@jkpCKZl>kFW?vP zOZa8{3Vs#8hTp_T@jLh!K8`=aC-CR^3;ZQMjlag<;IsG~{ssSvf5#VyoWv0g(GmkO z6C1IUG%}pzk`ZJi$sKWXu32mSGtklgVT;*-Q>IoXKTIFe8~fCZ8F_jAe?Ma%M6!gQ;R> zF?EcK;h09IiMg7&hPjrZOeb?4b3L1J0{WiH}6_*!^|^=+23!RO}rZlpo+lu)J{B_b`A(O0P5#X7S~Mimto z=M`lX78j1n7?E34no(FZYIsIbNp4=w$dcUb{MuZxzJ1>G&2i_L(7Np60cwIk3r`jN-iEWf>z% zM`dS>E-cH<$jd1ionM+gyd=LA_MaAA0rW0%2WPwaSzg`?v&{B}nybA2CLaeiE;a=k z7B+;y>Z9W7KObx89V!s;)w_e`k)d3yzPC=CCR#Pn#U^K4^M+e+^=Z96#IZ2{lX2%KdyR zfLtKh+br6%PcvbHX!rRrR;kAu?iAU|3^WU+_n<0Njb>5s8c`GCsEs7uKV6sq5PsseCi&@eK`GW0j3>q5(EY8SeirU{TBDB&gh90ri-MAxC~U91rV zFW6Y6-~?xItwD|lKyVAIphM{3=^AEyeZ)ax!pP)f#}(IjLtKlvQ&1H^q)=f)1IP26 z2W*t9;~NBntPJp=Q33yaK74HD(1(8{CN5rQwAh_PuE-poH|p?!frFIQt?p2B4L}9A zbduK}>W)>+Dh{*-d2X)ss0JM1S#nsVW;K_Zk6YU6_6u%WTv2Y}i=nuh>YN~pVDtoS zcWh#HWx&I;^214b!{rVzC4JpM=$@?E6ss_q=T-KRN{s+HZm*vU9!{~^E_cA1nPfc- z=+N}QfQM7l248l^>iuJ(<4{dk?BQYQ8JFKJy9>Hy<@ChHW>*W?$>PJgBSz+2rUa*L z%=#Df&(GTtmNX;ZX4PS+40fkwLJMl`q3a`Z* z@izPfeg;U(OF&iLz$fvyKu3Nj2*`+@3?|uRG#N`KkV#|;sRq*FA|A3(+6KYlO->9y z!w+(+Wb!M~4d8o9m$v#Omo~jE!3M+d(Suf@)u{cl+q>9$S9NG&@vNM@ zl3CTga{!bM&+C2dGi7y0E63-Dr*qA!a^>dqwiC^$3VepYsoVot;BEARca^rW2HoUh z6Dq2vwS|NQ_rg^V2p#|{0!BBZTTyx!x`htwLRA3r0JN!>hIkRdGpjPGEAv&7e^W=1By4?T*~x1#&e186gP5N$zQ(LU~_UQxs~27+91przFv^zs3}=!#~70K%+A z>MA)Xbr|eLEbQ=cwS6nC;g>dF$lD5>N|>lJ=xqTPGS%G?XbS;n`_no^P2j;OTcvi9 zYB3h_^mi7Z7T|44xJEZjP{P%>HA%d|8ZN37LMa}O=ub|Dubbvx>TU5Z<2+%Hq7i#H zfln5eC|Jl5^cp$}WF^lK~>G0n|hvxRF??#7F zd*6cE>8+f<+z<4QXOWAI56fdJ*BGh}%Aa3=yF8(E=Bn6-Cv4!juN=rO#1( z6^{$2V00h0VjH$&2X@k#w1&>5^XURwe*_Q4LtuA<@D+GiZSSlz#r1#;X45&B=cPr{ z!_|Jyc^RSicw@i1Bo8at<#hD9p##mad?Wk13e zsUM5SLFYQ^qI3E)%SeMFaR9e4K$w9_@X?JYb_=fJ2rdW7OvY32RIKQ$LNy_83)r2z zrImUpM*%`Q1vMU%m6atRHJ{az31N0HD{`28Ry#!CSph&D&a*Vb-P)Sr4e(hK^$9;j zzLV9LctS;(c!rxq-!DayB=J|`s{ojR6-1_v=w>&bAxQmFRS+EDYCIFqsuhU2bXv5T zHo4f6|M_+@mkI%1YcQ}-T38LPoeRr~*g!X)3y|EWKx#D~*SXjs(I_Tt-@U5T-_pCW zOW4z8w?Bg2u+@41Cl8(?q+9CU5OlyZ$oas4m%vHVMbslLr3p9Btf&IK=*CT=a!qc&jjs~h7K1pXk$t#DO3U;Ee*qG^v0s=< zNZDLeCmC`pcqF`-wgSz;A=peCUIJlXLmLnyf9RqC=4}KE-q!T}b(&$QX+```)`cIfNj;@203wTar?}17Z z&y+%5U||4j7gvI_p62xf@oEJsd&z1+H;Mz0Zgw|;PNiJ3O}T%Gl(qtXcJU}akvVhM6sDCMG zwFlj;Q( zevjVNg+HKch1`d+g!6^m(t${PE-H;kuao#wr0d2X<4@?#^pa&kFz{Oqp0jQ{H%4 zrVMd-IwbeV5HgfpL59&Dx{2OL@83i+NG865WYY)eL%>oVqeJHi1YYosH6n)>zST;q zAcaeU*rG8;G-L^%B*`aZg=t1X%s+-)NjK96=@z!m*t$>L!B5~hhc z`(c7WB9XWU=mj6}aY7h3)mtBgII+|(Y%&t3390)_wyut_q5kc!_^bAn<3%NZIAM|f7`20(+1titK>0d6ps;!>yfC#mvfBUSz8oDwV5VD>^Jkh_$U#>lx zs_3)T$T2n$4)PqtLw9$PM*6tmSTw+gqWURDWFquH%4tJ4SwwvF3HnSl6eR(|BV7+^ zC5uUr?x9c8r+UZ`ltz|-g{bM%)IJ}E>m~H_gty-N3Km`k?VILq@_R#V9!SJov!HJx zhFncLFB*R+jK2cxPfeetb_Ya~{d%k-t5G^+y@y8jcoVrfy2o4SehL5w*IneF!hY6~ zf6?dYi@p1~gRH-3zI$N44fJ`K?*(eFfrF87%~9a(zt$e|z(sw*0>~En66pKOf`3p$ z??|FSP;p1WynX#vzl}TzjDc(?kCGi^C)q_FBfH7tE9n^0T?J6Y5kfHXQU zwgBJm5n}eRKa(!xgqfsoLgvfg1S9&Tlufz6F#v>PMgYh_$lZT*G2bLy6T$6>I+7QN z4yczlA+0@Xr-!^lj*;W^7=53f5RSwGM{L~UT?))wqxPNWaKQ|3t56c)$F%kq zx*~(8_zBUj|zq>e|jsD7(19Vqh}0^kx6Du zjG3`8DU6k|(a-4@^h$X0Q|hFvGy*e;;<1KZXN!0;XPA zp_~4WxfDw!fOQa{9t{f3C- zmLQodwG z28Ni4XKyGFJ35X(;busHYu`0}3 znA?TR0HGz^Fae8U?hfBJ3E%$>qX-y8>*21_fB<>m2VZ00CmdJ7Pa)=^Ifik>E&cIKNSjNUUm{evkGn5&|WWdjja+o3pG@Y3QKQNlgR4|qB zGoxxykLAo(=8Q}$%a>Kj{IY;-t?Xvmdf67)L$XI?+hsdsyJWj%Psm=Dy(&8(dqZ|m zc1U(ac2ssuc3SqkTqAeNN6DwiYvhgc7I{FvSkB9r$^Rz5Mo#6|$?uczkspvBmwzn( zRDMeSx%^A{5AvVnXXU@hf0Lh&Nr)L7QxG#Zra8tNvoOXN(-uQxR>#~Pvp!~1%-)#S zV-Cg~ig`QcotWb>KgOINU>J}*z#Qv}ZI1m%?47au6={krMX6${qDC=KQKxV#8Wfyj zt>Sh?kK%sCX2n*;!-{Q+BZ?0cA1Y2LzEGS~Tu{a+)k>2xS2j%>RfZ~CHC#19Rj8^^RjOvFs#UX8b5yme zCY4vUNY$bWs1~buRhw$H>R#2usy(WARqw05P<^ZVUiG8uXVrPt1vR*IwM=bLr>n=S zC#ol_r>ZN|0reX7HuZk>3H6WaKUg!H!rE8|o5~JmhqA-i47QA|VVhaXu4A{ckFq=2 z$Joc&J?vBL*X&vN^&q2>YX)crX%w0`jZu@T8K%k5WNC6Vvo#Ae*J@U1dNkWLJ2bmA zyEXeXZ)gr`4rz{PPHMi^e5?6h^P}dEcpR^aXXE4J6XTQPo$*8C^Wux*r^Z*rSH{nX ze<%J#{Lk^fC9nyG1ZP58!jObO!qS8l39A!sOW2U`YQmcdzb3{csuGQfrbJ7kHF0QS zR^o`n$%)GnuTQ)?@!rJUiF*^DOMD^mrNo1YrxVX4exp@uHQEHNR-2?XXp^;OZHji3 zc8qqccD%MgTcj<~PSeiO)@tWz>$D5B^;(a1v6k1iY1_3cv@5l%v^Q$+(B7q8ul=`n zi}oSyBiilSXS92@`?b$&U(+7bzN>v-`=R!<_75G_F*>7PlrCEby9S5kM< z)})7%wk189v@_|kq{ox?Bt4b%O45O(H8F*F*Q4Q+;YLx*9xVU^)V z!%c>p4YwJ(4Vw)28#WsrGdye9XL!!=g5f2@LBk=#5yMf#F~hrtPYh=a=ZtEj$(U*! zY#eGFX3Q|=8Aln%7{?mN8w-qe#^uHh#>b5>7>^o{8Q(R&Z~V}B!uYZAQ{yS)S>rFp z-;C#t7m`sjNtPwYBo9o^OKwbFm;7Av*Cw-Ry6GCz8q>|DTTSasx0&uRZ8hyO?J+%N zdd9TZ^t$PY>8R0Q%F((! zRpvUg%e=r`Zw{D)=8(DF++kjBUSVEsUSnQszQufxxyO8;`2q8T=I!Pk=3VC9<|oY0 znGcwcn@^ZOHh*e9Wjn9svgBBDEh8=YmeH0gE#oW`EQOY0OR1&S zGS5o+4<)n3xRcAF=ldTr3)oQm6wGOwAu@+lPtrgap)>>p*ml%*%=Vt`1KUTouk6T9>@s_deV{$ouC%M{tUcbIXdh}HX3wx^ z*>mi<_L25{`)K=>_Hp(J_IdW@_IvI7?WY_HN0y_)(c-w#vCeV3<4(ujjvmKDjz=8Z z9XlMm9J?LQJ6>|U;yB=V!*S4Y#__A;0_1^+bAWS@Q{jwrnw+W5H0KcKNaq;mSm$_W zv9r`U(OK@C?=flqJ&YjNP&OOejoqL^!oo_pj zIp1}D;5^~{#Cgj3h4ZxYjPqQoDs^z`nAEFM8&g|TSEY8PZb?0v`fcj@v;k>sT0)vO zEh){JHaKl)T6$VmT29)Sv~g(@(hAcir%g$lmNqM`CT*@5k|RQ-C$mv{Mn2Q#|1WxJ BwgdnG literal 0 HcmV?d00001 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..a2d428945 --- /dev/null +++ b/Course/Course/Domain/Model/CourseDates.swift @@ -0,0 +1,210 @@ +// +// CourseDates.swift +// Course +// +// Created by Muhammad Umer on 10/18/23. +// + +import Foundation + +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: "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 "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 "Completed" + case .pastDue: + return "Past due" + case .dueNext: + return "Due next" + case .unreleased: + return "Unreleased" + case .verifiedOnly: + return "Verified Only" + 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..8e7446787 100644 --- a/Course/Course/Presentation/Container/CourseContainerView.swift +++ b/Course/Course/Presentation/Container/CourseContainerView.swift @@ -74,6 +74,15 @@ public struct CourseContainerView: View { } .tag(CourseTab.videos) + CourseDatesView(courseID: courseID, + viewModel: Container.shared.resolve(CourseDatesViewModel.self, + argument: courseID)!) + .tabItem { + CoreAssets.bookCircle.swiftUIImage.renderingMode(.template) + Text("Dates") + } + .tag("Dates") + DiscussionTopicsView(courseID: courseID, viewModel: Container.shared.resolve(DiscussionTopicsViewModel.self, argument: title)!, diff --git a/Course/Course/Presentation/Dates/CourseDatesView.swift b/Course/Course/Presentation/Dates/CourseDatesView.swift new file mode 100644 index 000000000..a19748e68 --- /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 ("Verified Only", Color.white, Color.black.opacity(0.5)) + case .pastDue: + return ("Past Due", Color.black, Color.gray.opacity(0.4)) + case .dueNext: + return ("Due Next", Color.white, Color.black.opacity(0.5)) + case .unreleased: + return ("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..c5866378c --- /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 + } + + public var sortedDates: [Date] { + courseDates?.sortedDateToCourseDateBlockDict.keys.sorted() ?? [] + } + + public 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) + guard let _ = courseDates?.courseDateBlocks else { + 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/CourseTests/Presentation/Unit/CourseDatesTests.swift b/Course/CourseTests/Presentation/Unit/CourseDatesTests.swift new file mode 100644 index 000000000..5189f718e --- /dev/null +++ b/Course/CourseTests/Presentation/Unit/CourseDatesTests.swift @@ -0,0 +1,31 @@ +// +// CourseDatesTests.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 CourseDatesTests: XCTestCase { + + func testBlockCompletionRequestSuccess() async throws { + let interactor = CourseInteractorProtocolMock() + let router = CourseRouterMock() + let connectivity = ConnectivityProtocolMock() + let cssInjector = CSSInjectorMock() + let analytics = CourseAnalyticsMock() + + let viewModel = CourseDatesViewModel( + interactor: interactor, + router: router, + cssInjector: cssInjector, + connectivity: connectivity, + courseID: "") + } +} 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/Discovery/Discovery.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate b/Discovery/Discovery.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..3427af208b18fb778ac03d5c459c711afe8ece44 GIT binary patch literal 15709 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 F{{kPd$U^`C literal 0 HcmV?d00001 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() + } } diff --git a/OpenEdX/Environment.swift b/OpenEdX/Environment.swift index e89c0bb88..cd693a480 100644 --- a/OpenEdX/Environment.swift +++ b/OpenEdX/Environment.swift @@ -28,22 +28,22 @@ class BuildConfiguration { var baseURL: String { switch environment { case .debugDev, .releaseDev: - return "https://example-dev.com" + return "https://raccoonapis.sandbox.edx.org" case .debugStage, .releaseStage: - return "https://example-stage.com" + return "https://raccoonapis.sandbox.edx.org" case .debugProd, .releaseProd: - return "https://example.com" + return "https://raccoonapis.sandbox.edx.org" } } var clientId: String { switch environment { case .debugDev, .releaseDev: - return "DEV_CLIENT_ID" + return "rg-edx-oauth-client-id" case .debugStage, .releaseStage: - return "STAGE_CLIENT_ID" + return "rg-edx-oauth-client-id" case .debugProd, .releaseProd: - return "PROD_CLIENT_ID" + return "rg-edx-oauth-client-id" } } diff --git a/Profile/Data/ProfileStorage.swift b/Profile/Data/ProfileStorage.swift index 2770f6060..89dc4e39c 100644 --- a/Profile/Data/ProfileStorage.swift +++ b/Profile/Data/ProfileStorage.swift @@ -9,7 +9,7 @@ import Foundation import Core public protocol ProfileStorage { - var userProfile: DataLayer.UserProfile? {get set} + var userProfile: DataLayer.UserProfile? { get set } } #if DEBUG From dd9b626378f9a1f1f2b5ff0eecf1c61b1c3d788e Mon Sep 17 00:00:00 2001 From: Muhammad Umer Date: Thu, 26 Oct 2023 11:50:45 +0100 Subject: [PATCH 02/10] add tests to course dates --- Course/Course.xcodeproj/project.pbxproj | 8 +- Course/CourseTests/CourseMock.generated.swift | 43 +- .../Unit/CourseDateViewModelTests.swift | 469 ++++++++++++++++++ .../Presentation/Unit/CourseDatesTests.swift | 31 -- 4 files changed, 515 insertions(+), 36 deletions(-) create mode 100644 Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift delete mode 100644 Course/CourseTests/Presentation/Unit/CourseDatesTests.swift diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index 98aeddf53..16f57b0fb 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -64,7 +64,7 @@ 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 /* CourseDatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB205BFA2AE81B1200136EC2 /* CourseDatesTests.swift */; }; + 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 */; }; @@ -155,7 +155,7 @@ 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 /* CourseDatesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDatesTests.swift; 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 = ""; }; @@ -441,7 +441,7 @@ children = ( 0262148E29AE17C4008BD75A /* HandoutsViewModelTests.swift */, 0295B1D8297E6DF8003B0C65 /* CourseUnitViewModelTests.swift */, - DB205BFA2AE81B1200136EC2 /* CourseDatesTests.swift */, + DB205BFA2AE81B1200136EC2 /* CourseDateViewModelTests.swift */, ); path = Unit; sourceTree = ""; @@ -687,7 +687,7 @@ 02F78AEB29E6BCA20038DE30 /* VideoPlayerViewModelTests.swift in Sources */, 023812E7297AC8EB0087098F /* CourseDetailsViewModelTests.swift in Sources */, 023812F3297AC9ED0087098F /* CourseMock.generated.swift in Sources */, - DB205BFB2AE81B1200136EC2 /* CourseDatesTests.swift in Sources */, + DB205BFB2AE81B1200136EC2 /* CourseDateViewModelTests.swift in Sources */, 022EA8CB297AD63B0014A8F7 /* CourseContainerViewModelTests.swift in Sources */, 0262148F29AE17C4008BD75A /* HandoutsViewModelTests.swift in Sources */, ); diff --git a/Course/CourseTests/CourseMock.generated.swift b/Course/CourseTests/CourseMock.generated.swift index a4cf6b418..2ff5465f5 100644 --- a/Course/CourseTests/CourseMock.generated.swift +++ b/Course/CourseTests/CourseMock.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.8.0 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 2.0.2 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT @@ -1671,6 +1671,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 +1700,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 +1760,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 +1782,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 +1798,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 +1842,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 +1952,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 +1978,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 +2018,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/Course/CourseTests/Presentation/Unit/CourseDatesTests.swift b/Course/CourseTests/Presentation/Unit/CourseDatesTests.swift deleted file mode 100644 index 5189f718e..000000000 --- a/Course/CourseTests/Presentation/Unit/CourseDatesTests.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// CourseDatesTests.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 CourseDatesTests: XCTestCase { - - func testBlockCompletionRequestSuccess() async throws { - let interactor = CourseInteractorProtocolMock() - let router = CourseRouterMock() - let connectivity = ConnectivityProtocolMock() - let cssInjector = CSSInjectorMock() - let analytics = CourseAnalyticsMock() - - let viewModel = CourseDatesViewModel( - interactor: interactor, - router: router, - cssInjector: cssInjector, - connectivity: connectivity, - courseID: "") - } -} From 03e03f6208161233c5b5b074fdfb287123907815 Mon Sep 17 00:00:00 2001 From: Muhammad Umer Date: Thu, 26 Oct 2023 12:56:09 +0100 Subject: [PATCH 03/10] cleanup --- Core/Core/Configuration/Config.swift | 2 +- Core/Core/Data/CoreStorage.swift | 8 ++++---- OpenEdX/Environment.swift | 12 ++++++------ Profile/Data/ProfileStorage.swift | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Core/Core/Configuration/Config.swift b/Core/Core/Configuration/Config.swift index 061364531..77f5da816 100644 --- a/Core/Core/Configuration/Config.swift +++ b/Core/Core/Configuration/Config.swift @@ -11,7 +11,7 @@ public class Config { public let baseURL: URL public let oAuthClientId: String - public let tokenType: TokenType = .bearer + public let tokenType: TokenType = .jwt public lazy var termsOfUse: URL? = { URL(string: "\(baseURL.description)/tos") diff --git a/Core/Core/Data/CoreStorage.swift b/Core/Core/Data/CoreStorage.swift index 749fb4a79..4ff71e963 100644 --- a/Core/Core/Data/CoreStorage.swift +++ b/Core/Core/Data/CoreStorage.swift @@ -8,10 +8,10 @@ import Foundation public protocol CoreStorage { - var accessToken: String? { get set } - var refreshToken: String? { get set } - var cookiesDate: String? { get set } - var user: DataLayer.User? { get set } + var accessToken: String? {get set} + var refreshToken: String? {get set} + var cookiesDate: String? {get set} + var user: DataLayer.User? {get set} var userSettings: UserSettings? {get set} func clear() } diff --git a/OpenEdX/Environment.swift b/OpenEdX/Environment.swift index cd693a480..e89c0bb88 100644 --- a/OpenEdX/Environment.swift +++ b/OpenEdX/Environment.swift @@ -28,22 +28,22 @@ class BuildConfiguration { var baseURL: String { switch environment { case .debugDev, .releaseDev: - return "https://raccoonapis.sandbox.edx.org" + return "https://example-dev.com" case .debugStage, .releaseStage: - return "https://raccoonapis.sandbox.edx.org" + return "https://example-stage.com" case .debugProd, .releaseProd: - return "https://raccoonapis.sandbox.edx.org" + return "https://example.com" } } var clientId: String { switch environment { case .debugDev, .releaseDev: - return "rg-edx-oauth-client-id" + return "DEV_CLIENT_ID" case .debugStage, .releaseStage: - return "rg-edx-oauth-client-id" + return "STAGE_CLIENT_ID" case .debugProd, .releaseProd: - return "rg-edx-oauth-client-id" + return "PROD_CLIENT_ID" } } diff --git a/Profile/Data/ProfileStorage.swift b/Profile/Data/ProfileStorage.swift index 89dc4e39c..2770f6060 100644 --- a/Profile/Data/ProfileStorage.swift +++ b/Profile/Data/ProfileStorage.swift @@ -9,7 +9,7 @@ import Foundation import Core public protocol ProfileStorage { - var userProfile: DataLayer.UserProfile? { get set } + var userProfile: DataLayer.UserProfile? {get set} } #if DEBUG From a82f9a7c4b55b638d1f18d17b3c2d1973b50c4ff Mon Sep 17 00:00:00 2001 From: Muhammad Umer Date: Thu, 26 Oct 2023 13:07:36 +0100 Subject: [PATCH 04/10] add analytics and translation --- .../Presentation/Container/CourseContainerView.swift | 7 +++++-- .../Container/CourseContainerViewModel.swift | 2 ++ Course/Course/Presentation/CourseAnalytics.swift | 2 ++ Course/Course/SwiftGen/Strings.swift | 2 ++ Course/Course/en.lproj/Localizable.strings | 1 + Course/Course/uk.lproj/Localizable.strings | 1 + OpenEdX/AnalyticsManager.swift | 9 +++++++++ 7 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift index 8e7446787..37387cb65 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 } @@ -79,9 +80,9 @@ public struct CourseContainerView: View { argument: courseID)!) .tabItem { CoreAssets.bookCircle.swiftUIImage.renderingMode(.template) - Text("Dates") + Text(CourseLocalization.CourseContainer.dates) } - .tag("Dates") + .tag(CourseTab.dates) DiscussionTopicsView(courseID: courseID, viewModel: Container.shared.resolve(DiscussionTopicsViewModel.self, @@ -131,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/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/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" From 4151eeed1816a5eb25942a946528ff6fd10d032b Mon Sep 17 00:00:00 2001 From: Muhammad Umer Date: Thu, 26 Oct 2023 13:52:32 +0100 Subject: [PATCH 05/10] regenerate CourseMock for analytics --- Course/CourseTests/CourseMock.generated.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Course/CourseTests/CourseMock.generated.swift b/Course/CourseTests/CourseMock.generated.swift index 2ff5465f5..8d1c98b76 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) } From f57c13db5b933469a53b873db32a6103c2d63f64 Mon Sep 17 00:00:00 2001 From: Muhammad Umer Date: Wed, 1 Nov 2023 00:10:13 +0500 Subject: [PATCH 06/10] chore: add translations --- Core/Core/SwiftGen/Strings.swift | 14 ++++++++++++++ Core/Core/en.lproj/Localizable.strings | 7 +++++++ Core/Core/uk.lproj/Localizable.strings | 7 +++++++ Course/Course/Domain/Model/CourseDates.swift | 13 +++++++------ .../Container/CourseContainerView.swift | 2 +- .../Presentation/Dates/CourseDatesView.swift | 8 ++++---- .../Presentation/Dates/CourseDatesViewModel.swift | 6 +++--- 7 files changed, 43 insertions(+), 14 deletions(-) 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/Domain/Model/CourseDates.swift b/Course/Course/Domain/Model/CourseDates.swift index a2d428945..3b5c8a718 100644 --- a/Course/Course/Domain/Model/CourseDates.swift +++ b/Course/Course/Domain/Model/CourseDates.swift @@ -6,6 +6,7 @@ // import Foundation +import Core public struct CourseDates { let datesBannerInfo: DatesBannerInfo @@ -36,7 +37,7 @@ public struct CourseDates { description: "", learnerHasAccess: true, link: "", linkText: nil, - title: "Today", + title: CoreLocalization.CourseDates.today, extraInfo: nil, firstComponentBlockID: "uniqueIDForToday") dateToCourseDateBlockDict[today] = [todayBlock] @@ -194,15 +195,15 @@ public enum BlockStatus { var title: String { switch self { case .completed: - return "Completed" + return CoreLocalization.CourseDates.completed case .pastDue: - return "Past due" + return CoreLocalization.CourseDates.pastDue case .dueNext: - return "Due next" + return CoreLocalization.CourseDates.dueNext case .unreleased: - return "Unreleased" + return CoreLocalization.CourseDates.unreleased case .verifiedOnly: - return "Verified Only" + return CoreLocalization.CourseDates.verifiedOnly default: return "" } diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift index 37387cb65..726cdaeb3 100644 --- a/Course/Course/Presentation/Container/CourseContainerView.swift +++ b/Course/Course/Presentation/Container/CourseContainerView.swift @@ -79,7 +79,7 @@ public struct CourseContainerView: View { viewModel: Container.shared.resolve(CourseDatesViewModel.self, argument: courseID)!) .tabItem { - CoreAssets.bookCircle.swiftUIImage.renderingMode(.template) + Image(systemName: "calendar").renderingMode(.template) Text(CourseLocalization.CourseContainer.dates) } .tag(CourseTab.dates) diff --git a/Course/Course/Presentation/Dates/CourseDatesView.swift b/Course/Course/Presentation/Dates/CourseDatesView.swift index a19748e68..34774e1f0 100644 --- a/Course/Course/Presentation/Dates/CourseDatesView.swift +++ b/Course/Course/Presentation/Dates/CourseDatesView.swift @@ -233,13 +233,13 @@ struct BlockStatusView: View { func getStatusDetails(for blockStatus: BlockStatus) -> (String, Color, Color) { switch blockStatus { case .verifiedOnly: - return ("Verified Only", Color.white, Color.black.opacity(0.5)) + return (CoreLocalization.CourseDates.verifiedOnly, Color.white, Color.black.opacity(0.5)) case .pastDue: - return ("Past Due", Color.black, Color.gray.opacity(0.4)) + return (CoreLocalization.CourseDates.pastDue, Color.black, Color.gray.opacity(0.4)) case .dueNext: - return ("Due Next", Color.white, Color.black.opacity(0.5)) + return (CoreLocalization.CourseDates.dueNext, Color.white, Color.black.opacity(0.5)) case .unreleased: - return ("Unreleased", Color.white.opacity(0), Color.white.opacity(0)) + return (CoreLocalization.CourseDates.unreleased, Color.white.opacity(0), Color.white.opacity(0)) default: return ("", Color.white.opacity(0), Color.white.opacity(0)) } diff --git a/Course/Course/Presentation/Dates/CourseDatesViewModel.swift b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift index c5866378c..e60d413d9 100644 --- a/Course/Course/Presentation/Dates/CourseDatesViewModel.swift +++ b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift @@ -41,11 +41,11 @@ public class CourseDatesViewModel: ObservableObject { self.connectivity = connectivity } - public var sortedDates: [Date] { + var sortedDates: [Date] { courseDates?.sortedDateToCourseDateBlockDict.keys.sorted() ?? [] } - public func blocks(for date: Date) -> [CourseDateBlock] { + func blocks(for date: Date) -> [CourseDateBlock] { courseDates?.sortedDateToCourseDateBlockDict[date] ?? [] } @@ -54,7 +54,7 @@ public class CourseDatesViewModel: ObservableObject { isShowProgress = true do { courseDates = try await interactor.getCourseDates(courseID: courseID) - guard let _ = courseDates?.courseDateBlocks else { + if courseDates?.courseDateBlocks == nil { isShowProgress = false errorMessage = CoreLocalization.Error.unknownError return From af30af9a2a3720d86933de9fb14ae8b45bbfbaf6 Mon Sep 17 00:00:00 2001 From: Muhammad Umer Date: Wed, 1 Nov 2023 09:58:45 +0500 Subject: [PATCH 07/10] chore: update .gitignore and remove files --- .gitignore | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 36e2b2501..6ec21510b 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,13 @@ DerivedData/ *.perspectivev3 !default.perspectivev3 +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +**/xcshareddata/WorkspaceSettings.xcsettings + ## Obj-C/Swift specific *.hmap @@ -100,4 +108,4 @@ iOSInjectionProject/ xcode-frameworks vendor/ -.bundle/ \ No newline at end of file +.bundle/ From 4c51b8417812d49b6074eb99ca6ef998996f2fda Mon Sep 17 00:00:00 2001 From: Muhammad Umer Date: Wed, 1 Nov 2023 12:14:53 +0500 Subject: [PATCH 08/10] fix: remove gitignored file --- .../UserInterfaceState.xcuserstate | Bin 10773 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 OpenEdX.xcodeproj/project.xcworkspace/xcuserdata/vchekyrta.xcuserdatad/UserInterfaceState.xcuserstate 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 b59556e654f35a351b926f65bd11ffd1fa0a1c57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10773 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 ab8F`FnQvz9i@F#p{z|kl=OX@QzW1L+M1ZCM From cc8ac50d82dc46f0ae67c960760700e1f7fb806e Mon Sep 17 00:00:00 2001 From: Muhammad Umer Date: Wed, 1 Nov 2023 14:17:33 +0500 Subject: [PATCH 09/10] fix: update .gitignore --- .gitignore | 2 ++ Course/Course/Domain/Model/CourseDates.swift | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6ec21510b..582c86cb2 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,8 @@ DerivedData/ !default.perspectivev3 *.xcodeproj/* +**/xcuserdata/ +**/*.xcuserdata/* !*.xcodeproj/project.pbxproj !*.xcodeproj/xcshareddata/ !*.xcodeproj/project.xcworkspace/ diff --git a/Course/Course/Domain/Model/CourseDates.swift b/Course/Course/Domain/Model/CourseDates.swift index 3b5c8a718..e60265941 100644 --- a/Course/Course/Domain/Model/CourseDates.swift +++ b/Course/Course/Domain/Model/CourseDates.swift @@ -88,7 +88,7 @@ public struct CourseDateBlock { var blockTitle: String { if isToday { - return "Today" + return CoreLocalization.CourseDates.today } else { return blockStatus.title } From 64d58c16acece921755e603bf4918488c13c3a03 Mon Sep 17 00:00:00 2001 From: Muhammad Umer Date: Wed, 1 Nov 2023 14:20:43 +0500 Subject: [PATCH 10/10] fix: another try on removing files --- .../UserInterfaceState.xcuserstate | Bin 10771 -> 0 bytes .../UserInterfaceState.xcuserstate | Bin 12693 -> 0 bytes .../UserInterfaceState.xcuserstate | Bin 11709 -> 0 bytes .../UserInterfaceState.xcuserstate | Bin 15709 -> 0 bytes .../xcshareddata/WorkspaceSettings.xcsettings | 5 ----- 5 files changed, 5 deletions(-) delete mode 100644 Core/Core.xcodeproj.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate delete mode 100644 Core/Core.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate delete mode 100644 Course/Course.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate delete mode 100644 Discovery/Discovery.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate delete mode 100644 OpenEdX.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/Core/Core.xcodeproj.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate b/Core/Core.xcodeproj.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index dfb82c0a5b97876276784a124fd56bdf8b85b22e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10771 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 sIX`rMw=6BSNHaCLn!B|EB5w2Uh8c#sB~S diff --git a/Core/Core.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate b/Core/Core.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 81b98d607430a7a2b00037a2d008c07b0530118c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12693 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 diff --git a/Course/Course.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate b/Course/Course.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 4fe0ac86f3c7c5ade1e146f3c9281fd39d96ee8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11709 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 BwgdnG diff --git a/Discovery/Discovery.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate b/Discovery/Discovery.xcodeproj/project.xcworkspace/xcuserdata/m.umer.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 3427af208b18fb778ac03d5c459c711afe8ece44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15709 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 F{{kPd$U^`C diff --git a/OpenEdX.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/OpenEdX.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index 0c67376eb..000000000 --- a/OpenEdX.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,5 +0,0 @@ - - - - -