diff --git a/Teacher/Teacher.xcodeproj/project.pbxproj b/Teacher/Teacher.xcodeproj/project.pbxproj index 82ac36bc60..74a2791d4d 100644 --- a/Teacher/Teacher.xcodeproj/project.pbxproj +++ b/Teacher/Teacher.xcodeproj/project.pbxproj @@ -28,10 +28,10 @@ 494AE0BA1F843CB5001A8F31 /* Routes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494AE0B61F843CB4001A8F31 /* Routes.swift */; }; 61BD85FC1EB2933C005A09A5 /* TeacherAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61BD85FB1EB2933C005A09A5 /* TeacherAppDelegate.swift */; }; 78AD913121C8180C0075FBCF /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 78AD913021C8180C0075FBCF /* GoogleService-Info.plist */; }; - 7D0A28E2251A8284004FB3B6 /* SubmissionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0A28E1251A8284004FB3B6 /* SubmissionHeader.swift */; }; + 7D0A28E2251A8284004FB3B6 /* SubmissionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0A28E1251A8284004FB3B6 /* SubmissionHeaderView.swift */; }; 7D0D7AE2251E6E6C00550AEC /* SubmissionListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0D7AE1251E6E6C00550AEC /* SubmissionListViewController.swift */; }; 7D0D7B36251EA41B00550AEC /* SubmissionListViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7D0D7B35251EA41B00550AEC /* SubmissionListViewController.storyboard */; }; - 7D264DC72537A6E1007CDD90 /* SimilarityScore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D264DC62537A6E1007CDD90 /* SimilarityScore.swift */; }; + 7D264DC72537A6E1007CDD90 /* SimilarityScoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D264DC62537A6E1007CDD90 /* SimilarityScoreView.swift */; }; 7D3DC0C0252291B400B0EBC1 /* SubmissionFilterPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D3DC0BF252291B400B0EBC1 /* SubmissionFilterPickerViewController.swift */; }; 7D3DC0C7252432DD00B0EBC1 /* SubmissionFilterPickerViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D3DC0C6252432DD00B0EBC1 /* SubmissionFilterPickerViewControllerTests.swift */; }; 7D3DC0CB252432ED00B0EBC1 /* SubmissionListViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D3DC0CA252432ED00B0EBC1 /* SubmissionListViewControllerTests.swift */; }; @@ -53,14 +53,14 @@ 7D64A983236796CC004EAEDF /* AttendanceStatusControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D64A982236796CC004EAEDF /* AttendanceStatusControllerTests.swift */; }; 7D64A98523689DD5004EAEDF /* DatePickerViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D64A98423689DD4004EAEDF /* DatePickerViewControllerTests.swift */; }; 7D64A9872368C6C0004EAEDF /* AttendanceViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D64A9862368C6C0004EAEDF /* AttendanceViewControllerTests.swift */; }; - 7D7126B12534B980000DEDE4 /* Drawer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D7126B02534B980000DEDE4 /* Drawer.swift */; }; + 7D7126B12534B980000DEDE4 /* DrawerContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D7126B02534B980000DEDE4 /* DrawerContainer.swift */; }; 7D7126BC25366DAB000DEDE4 /* SubmissionViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D7126BB25366DAB000DEDE4 /* SubmissionViewer.swift */; }; 7D7126C025374446000DEDE4 /* SubmissionGrades.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D7126BF25374446000DEDE4 /* SubmissionGrades.swift */; }; - 7D7126C425374457000DEDE4 /* SubmissionCommentList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D7126C325374457000DEDE4 /* SubmissionCommentList.swift */; }; + 7D7126C425374457000DEDE4 /* SubmissionCommentListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D7126C325374457000DEDE4 /* SubmissionCommentListView.swift */; }; 7D7126C725374469000DEDE4 /* SubmissionFileList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D7126C625374469000DEDE4 /* SubmissionFileList.swift */; }; 7D742F382554D01400FD6CF1 /* RubricsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D742F372554D01400FD6CF1 /* RubricsView.swift */; }; - 7D742F452556566F00FD6CF1 /* CommentEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D742F442556566F00FD6CF1 /* CommentEditor.swift */; }; - 7D7D6D5225102052002B1485 /* SubmissionGrader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D7D6D5125102052002B1485 /* SubmissionGrader.swift */; }; + 7D742F452556566F00FD6CF1 /* CommentEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D742F442556566F00FD6CF1 /* CommentEditorView.swift */; }; + 7D7D6D5225102052002B1485 /* SubmissionGraderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D7D6D5125102052002B1485 /* SubmissionGraderView.swift */; }; 7DA4DA302440EFBA009CA964 /* IBDesignables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DA4DA2F2440EFBA009CA964 /* IBDesignables.swift */; }; 7DBF923B2269791600710D0F /* TestsFoundation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B15CA26C221DDEB40014FB02 /* TestsFoundation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 7DC7047A24ABFD4800CD1C46 /* GetRootFolders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DC7047924ABFD4800CD1C46 /* GetRootFolders.swift */; }; @@ -105,6 +105,7 @@ CF0605E32D95C5A50014C2EC /* RubricCircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF0605E22D95C5A00014C2EC /* RubricCircle.swift */; }; CF06060C2D96EA100014C2EC /* RubricCriterionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF06060B2D96EA100014C2EC /* RubricCriterionView.swift */; }; CF0606122D9A9A990014C2EC /* RubricsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF0606112D9A9A950014C2EC /* RubricsViewModel.swift */; }; + CF0E06942DA8FFC6007AC131 /* SubmissionGraderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF0E06932DA8FFC4007AC131 /* SubmissionGraderViewModel.swift */; }; CF13E54C299D090500F57F69 /* QuizSubmissionListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF13E54B299D090500F57F69 /* QuizSubmissionListViewModel.swift */; }; CF13E550299D0E0B00F57F69 /* QuizSubmissionListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF13E54F299D0E0B00F57F69 /* QuizSubmissionListInteractor.swift */; }; CF13E552299D0EE100F57F69 /* QuizSubmissionListInteractorLive.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF13E551299D0EE100F57F69 /* QuizSubmissionListInteractorLive.swift */; }; @@ -114,7 +115,7 @@ CF20B5C02B1F9C7F00C00B39 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CF20B5BF2B1F9C7F00C00B39 /* LaunchScreen.storyboard */; }; CF28C6BD2DA4024A007CD65A /* SpeedGraderInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF28C6BC2DA40246007CD65A /* SpeedGraderInteractor.swift */; }; CF4C5F7C2DA02401005AE000 /* SpeedGraderInteractorLive.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF4C5F7B2DA023FB005AE000 /* SpeedGraderInteractorLive.swift */; }; - CF56B71525F68CD3000DD32F /* SubmissionHeaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF56B71425F68CD3000DD32F /* SubmissionHeaderTests.swift */; }; + CF56B71525F68CD3000DD32F /* SubmissionHeaderViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF56B71425F68CD3000DD32F /* SubmissionHeaderViewTests.swift */; }; CF67D00A2B18E9A1009D826A /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = CF67D0092B18E9A1009D826A /* InfoPlist.xcstrings */; }; CF67D00C2B18E9A1009D826A /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = CF67D00B2B18E9A1009D826A /* InfoPlist.xcstrings */; }; CF67D00E2B18E9A1009D826A /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = CF67D00D2B18E9A1009D826A /* Localizable.xcstrings */; }; @@ -239,10 +240,10 @@ 497CA7321F82ACF400501613 /* CanvasCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = CanvasCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 61BD85FB1EB2933C005A09A5 /* TeacherAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeacherAppDelegate.swift; sourceTree = ""; }; 78AD913021C8180C0075FBCF /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; - 7D0A28E1251A8284004FB3B6 /* SubmissionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionHeader.swift; sourceTree = ""; }; + 7D0A28E1251A8284004FB3B6 /* SubmissionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionHeaderView.swift; sourceTree = ""; }; 7D0D7AE1251E6E6C00550AEC /* SubmissionListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionListViewController.swift; sourceTree = ""; }; 7D0D7B35251EA41B00550AEC /* SubmissionListViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = SubmissionListViewController.storyboard; sourceTree = ""; }; - 7D264DC62537A6E1007CDD90 /* SimilarityScore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimilarityScore.swift; sourceTree = ""; }; + 7D264DC62537A6E1007CDD90 /* SimilarityScoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimilarityScoreView.swift; sourceTree = ""; }; 7D3DA83821876142000D828F /* Core.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7D3DC0BF252291B400B0EBC1 /* SubmissionFilterPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionFilterPickerViewController.swift; sourceTree = ""; }; 7D3DC0C6252432DD00B0EBC1 /* SubmissionFilterPickerViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionFilterPickerViewControllerTests.swift; sourceTree = ""; }; @@ -268,14 +269,14 @@ 7D64A982236796CC004EAEDF /* AttendanceStatusControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttendanceStatusControllerTests.swift; sourceTree = ""; }; 7D64A98423689DD4004EAEDF /* DatePickerViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerViewControllerTests.swift; sourceTree = ""; }; 7D64A9862368C6C0004EAEDF /* AttendanceViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttendanceViewControllerTests.swift; sourceTree = ""; }; - 7D7126B02534B980000DEDE4 /* Drawer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Drawer.swift; sourceTree = ""; }; + 7D7126B02534B980000DEDE4 /* DrawerContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawerContainer.swift; sourceTree = ""; }; 7D7126BB25366DAB000DEDE4 /* SubmissionViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionViewer.swift; sourceTree = ""; }; 7D7126BF25374446000DEDE4 /* SubmissionGrades.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionGrades.swift; sourceTree = ""; }; - 7D7126C325374457000DEDE4 /* SubmissionCommentList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionCommentList.swift; sourceTree = ""; }; + 7D7126C325374457000DEDE4 /* SubmissionCommentListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionCommentListView.swift; sourceTree = ""; }; 7D7126C625374469000DEDE4 /* SubmissionFileList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionFileList.swift; sourceTree = ""; }; 7D742F372554D01400FD6CF1 /* RubricsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RubricsView.swift; sourceTree = ""; }; - 7D742F442556566F00FD6CF1 /* CommentEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentEditor.swift; sourceTree = ""; }; - 7D7D6D5125102052002B1485 /* SubmissionGrader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionGrader.swift; sourceTree = ""; }; + 7D742F442556566F00FD6CF1 /* CommentEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentEditorView.swift; sourceTree = ""; }; + 7D7D6D5125102052002B1485 /* SubmissionGraderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionGraderView.swift; sourceTree = ""; }; 7DA4DA2F2440EFBA009CA964 /* IBDesignables.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IBDesignables.swift; sourceTree = ""; }; 7DC7047924ABFD4800CD1C46 /* GetRootFolders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetRootFolders.swift; sourceTree = ""; }; 7DCA320B2541D38F00458651 /* URLSubmissionViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSubmissionViewer.swift; sourceTree = ""; }; @@ -316,6 +317,7 @@ CF0605E22D95C5A00014C2EC /* RubricCircle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RubricCircle.swift; sourceTree = ""; }; CF06060B2D96EA100014C2EC /* RubricCriterionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RubricCriterionView.swift; sourceTree = ""; }; CF0606112D9A9A950014C2EC /* RubricsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RubricsViewModel.swift; sourceTree = ""; }; + CF0E06932DA8FFC4007AC131 /* SubmissionGraderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionGraderViewModel.swift; sourceTree = ""; }; CF13E54B299D090500F57F69 /* QuizSubmissionListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizSubmissionListViewModel.swift; sourceTree = ""; }; CF13E54F299D0E0B00F57F69 /* QuizSubmissionListInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizSubmissionListInteractor.swift; sourceTree = ""; }; CF13E551299D0EE100F57F69 /* QuizSubmissionListInteractorLive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizSubmissionListInteractorLive.swift; sourceTree = ""; }; @@ -325,7 +327,7 @@ CF20B5BF2B1F9C7F00C00B39 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; CF28C6BC2DA40246007CD65A /* SpeedGraderInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeedGraderInteractor.swift; sourceTree = ""; }; CF4C5F7B2DA023FB005AE000 /* SpeedGraderInteractorLive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeedGraderInteractorLive.swift; sourceTree = ""; }; - CF56B71425F68CD3000DD32F /* SubmissionHeaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionHeaderTests.swift; sourceTree = ""; }; + CF56B71425F68CD3000DD32F /* SubmissionHeaderViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionHeaderViewTests.swift; sourceTree = ""; }; CF67D0092B18E9A1009D826A /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; CF67D00B2B18E9A1009D826A /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; CF67D00D2B18E9A1009D826A /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; @@ -799,8 +801,8 @@ CF0605CE2D95A5790014C2EC /* View */ = { isa = PBXGroup; children = ( - 7D742F442556566F00FD6CF1 /* CommentEditor.swift */, - 7D7126C325374457000DEDE4 /* SubmissionCommentList.swift */, + 7D742F442556566F00FD6CF1 /* CommentEditorView.swift */, + 7D7126C325374457000DEDE4 /* SubmissionCommentListView.swift */, 7D4B2747253F2D1600A98DE3 /* SubmissionCommentListCell.swift */, ); path = View; @@ -890,6 +892,7 @@ CF0605D82D95A9340014C2EC /* MainLayout */ = { isa = PBXGroup; children = ( + CF0E06922DA8FEF9007AC131 /* ViewModel */, CF0605D92D95A93D0014C2EC /* View */, ); path = MainLayout; @@ -898,10 +901,10 @@ CF0605D92D95A93D0014C2EC /* View */ = { isa = PBXGroup; children = ( - 7D7126B02534B980000DEDE4 /* Drawer.swift */, - 7D0A28E1251A8284004FB3B6 /* SubmissionHeader.swift */, - 7D7D6D5125102052002B1485 /* SubmissionGrader.swift */, - 7D264DC62537A6E1007CDD90 /* SimilarityScore.swift */, + 7D7126B02534B980000DEDE4 /* DrawerContainer.swift */, + 7D0A28E1251A8284004FB3B6 /* SubmissionHeaderView.swift */, + 7D7D6D5125102052002B1485 /* SubmissionGraderView.swift */, + 7D264DC62537A6E1007CDD90 /* SimilarityScoreView.swift */, ); path = View; sourceTree = ""; @@ -990,7 +993,7 @@ CF0606012D96E47E0014C2EC /* View */ = { isa = PBXGroup; children = ( - CF56B71425F68CD3000DD32F /* SubmissionHeaderTests.swift */, + CF56B71425F68CD3000DD32F /* SubmissionHeaderViewTests.swift */, ); path = View; sourceTree = ""; @@ -1037,6 +1040,14 @@ path = ViewModel; sourceTree = ""; }; + CF0E06922DA8FEF9007AC131 /* ViewModel */ = { + isa = PBXGroup; + children = ( + CF0E06932DA8FFC4007AC131 /* SubmissionGraderViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; CF13E549299D08E000F57F69 /* QuizSubmissionList */ = { isa = PBXGroup; children = ( @@ -1470,20 +1481,20 @@ CF4C5F7C2DA02401005AE000 /* SpeedGraderInteractorLive.swift in Sources */, CF93FCB82D5A5AF600C4538F /* SpeedGraderAssembly.swift in Sources */, 7D3DC0C0252291B400B0EBC1 /* SubmissionFilterPickerViewController.swift in Sources */, - 7D7126C425374457000DEDE4 /* SubmissionCommentList.swift in Sources */, + 7D7126C425374457000DEDE4 /* SubmissionCommentListView.swift in Sources */, 7DCA320C2541D38F00458651 /* URLSubmissionViewer.swift in Sources */, CFCADAF02D9D6E1D003796E7 /* RubricRatingViewModel.swift in Sources */, 7D742F382554D01400FD6CF1 /* RubricsView.swift in Sources */, CF13E550299D0E0B00F57F69 /* QuizSubmissionListInteractor.swift in Sources */, 494AE0BA1F843CB5001A8F31 /* Routes.swift in Sources */, CF13E559299D1AEC00F57F69 /* QuizSubmissionListView.swift in Sources */, - 7D7126B12534B980000DEDE4 /* Drawer.swift in Sources */, + 7D7126B12534B980000DEDE4 /* DrawerContainer.swift in Sources */, 3B55FED522FDE70F00FCB7B2 /* HideGradesViewController.swift in Sources */, - 7D0A28E2251A8284004FB3B6 /* SubmissionHeader.swift in Sources */, + 7D0A28E2251A8284004FB3B6 /* SubmissionHeaderView.swift in Sources */, 7DCA321A25423CDF00458651 /* SpeedGraderViewController.swift in Sources */, CF0605E32D95C5A50014C2EC /* RubricCircle.swift in Sources */, CFCADAF42D9D871D003796E7 /* RubricRatingView.swift in Sources */, - 7D742F452556566F00FD6CF1 /* CommentEditor.swift in Sources */, + 7D742F452556566F00FD6CF1 /* CommentEditorView.swift in Sources */, CF28C6BD2DA4024A007CD65A /* SpeedGraderInteractor.swift in Sources */, CFCADAFB2D9D9AE0003796E7 /* RubricGradingInteractor.swift in Sources */, EBAF32F627AACFC9000ACD32 /* SubmissionCommentLibraryViewModel.swift in Sources */, @@ -1502,8 +1513,9 @@ 7D64A95E23620A27004EAEDF /* AttendanceViewController.swift in Sources */, D91C051F29AE107B0032BFB1 /* QuizSubmissionListItemViewModel.swift in Sources */, B4707B052B1DDF7900F67CA8 /* SubmissionCommentListViewModel.swift in Sources */, - 7D264DC72537A6E1007CDD90 /* SimilarityScore.swift in Sources */, + 7D264DC72537A6E1007CDD90 /* SimilarityScoreView.swift in Sources */, CFCADAF22D9D6FE4003796E7 /* RubricCustomRatingViewModel.swift in Sources */, + CF0E06942DA8FFC6007AC131 /* SubmissionGraderViewModel.swift in Sources */, D9624A5229B0E90800F5303F /* QuizSubmissionListFilter.swift in Sources */, D9624A5029AF94D000F5303F /* QuizSubmissionListInteractorPreview.swift in Sources */, CF13E54C299D090500F57F69 /* QuizSubmissionListViewModel.swift in Sources */, @@ -1523,7 +1535,7 @@ EBE802AF27B41C5F0003A663 /* CommentLibrarySheet.swift in Sources */, 494AE0B91F843CB5001A8F31 /* TeacherTabBarController.swift in Sources */, CFCADAEA2D9BE21F003796E7 /* RubricCriterionViewModel.swift in Sources */, - 7D7D6D5225102052002B1485 /* SubmissionGrader.swift in Sources */, + 7D7D6D5225102052002B1485 /* SubmissionGraderView.swift in Sources */, D9624A6829B8A05600F5303F /* QuizSubmissionListItemArrayFilter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1546,7 +1558,7 @@ 7D3DC0C7252432DD00B0EBC1 /* SubmissionFilterPickerViewControllerTests.swift in Sources */, 7D64A98123676E36004EAEDF /* RollCallSessionTests.swift in Sources */, 7D64A96D23626C22004EAEDF /* StatusCellTests.swift in Sources */, - CF56B71525F68CD3000DD32F /* SubmissionHeaderTests.swift in Sources */, + CF56B71525F68CD3000DD32F /* SubmissionHeaderViewTests.swift in Sources */, D9624A7E29C4AEF000F5303F /* QuizSubmissionListItemTests.swift in Sources */, D9B4850029CCA8090004EBB5 /* QuizSubmissionListInteractorLiveTests.swift in Sources */, 7D64A983236796CC004EAEDF /* AttendanceStatusControllerTests.swift in Sources */, diff --git a/Teacher/Teacher/SpeedGrader/CommentLibrary/View/CommentLibrarySheet.swift b/Teacher/Teacher/SpeedGrader/CommentLibrary/View/CommentLibrarySheet.swift index 4ceb7e7638..919e50976d 100644 --- a/Teacher/Teacher/SpeedGrader/CommentLibrary/View/CommentLibrarySheet.swift +++ b/Teacher/Teacher/SpeedGrader/CommentLibrary/View/CommentLibrarySheet.swift @@ -32,7 +32,7 @@ struct CommentLibrarySheet: View { CommentLibraryList(viewModel: viewModel, comment: $comment) { presentationMode.wrappedValue.dismiss() } - CommentEditor( + CommentEditorView( text: $comment, shouldShowCommentLibrary: false, showCommentLibrary: .constant(false), diff --git a/Teacher/Teacher/SpeedGrader/Comments/View/CommentEditor.swift b/Teacher/Teacher/SpeedGrader/Comments/View/CommentEditorView.swift similarity index 97% rename from Teacher/Teacher/SpeedGrader/Comments/View/CommentEditor.swift rename to Teacher/Teacher/SpeedGrader/Comments/View/CommentEditorView.swift index b8c426672d..fa215d2fb2 100644 --- a/Teacher/Teacher/SpeedGrader/Comments/View/CommentEditor.swift +++ b/Teacher/Teacher/SpeedGrader/Comments/View/CommentEditorView.swift @@ -19,7 +19,7 @@ import SwiftUI import Core -struct CommentEditor: View { +struct CommentEditorView: View { @Environment(\.viewController) var controller @Binding var text: String @@ -77,7 +77,7 @@ struct CommentEditor: View { struct CommentEditor_Previews: PreviewProvider { static var previews: some View { @State var showCommentLibrary = false - CommentEditor(text: .constant("Sample Text"), + CommentEditorView(text: .constant("Sample Text"), shouldShowCommentLibrary: true, showCommentLibrary: $showCommentLibrary, action: {}, diff --git a/Teacher/Teacher/SpeedGrader/Comments/View/SubmissionCommentListCell.swift b/Teacher/Teacher/SpeedGrader/Comments/View/SubmissionCommentListCell.swift index 43144973e5..9b2f766153 100644 --- a/Teacher/Teacher/SpeedGrader/Comments/View/SubmissionCommentListCell.swift +++ b/Teacher/Teacher/SpeedGrader/Comments/View/SubmissionCommentListCell.swift @@ -24,7 +24,7 @@ struct SubmissionCommentListCell: View { let submission: Submission let comment: SubmissionComment - @Binding var attempt: Int? + @Binding var attempt: Int @Binding var fileID: String? @Environment(\.appEnvironment.currentSession?.userID) var currentUserID diff --git a/Teacher/Teacher/SpeedGrader/Comments/View/SubmissionCommentList.swift b/Teacher/Teacher/SpeedGrader/Comments/View/SubmissionCommentListView.swift similarity index 96% rename from Teacher/Teacher/SpeedGrader/Comments/View/SubmissionCommentList.swift rename to Teacher/Teacher/SpeedGrader/Comments/View/SubmissionCommentListView.swift index 2fa36a2d9c..604bbcb298 100644 --- a/Teacher/Teacher/SpeedGrader/Comments/View/SubmissionCommentList.swift +++ b/Teacher/Teacher/SpeedGrader/Comments/View/SubmissionCommentListView.swift @@ -20,11 +20,11 @@ import SwiftUI import Core import Combine -struct SubmissionCommentList: View { +struct SubmissionCommentListView: View { let assignment: Assignment let submission: Submission let filePicker = FilePicker(env: .shared) - @Binding var attempt: Int? + @Binding var attempt: Int @Binding var fileID: String? @Binding var showRecorder: MediaCommentType? @Binding var comment: String @@ -32,7 +32,7 @@ struct SubmissionCommentList: View { @Environment(\.appEnvironment) var env @Environment(\.viewController) var controller - @ObservedObject var attempts: Store> + var attempts: [Submission] @ObservedObject var commentLibrary: SubmissionCommentLibraryViewModel @StateObject private var viewModel: SubmissionCommentListViewModel @@ -40,18 +40,18 @@ struct SubmissionCommentList: View { @State var showMediaOptions = false @State var showCommentLibrary = false - @AccessibilityFocusState private var focusedTab: SubmissionGrader.GraderTab? + @AccessibilityFocusState private var focusedTab: SubmissionGraderView.GraderTab? init( assignment: Assignment, submission: Submission, - attempts: Store>, - attempt: Binding, + attempts: [Submission], + attempt: Binding, fileID: Binding, showRecorder: Binding, enteredComment: Binding, commentLibrary: SubmissionCommentLibraryViewModel, - focusedTab: AccessibilityFocusState + focusedTab: AccessibilityFocusState ) { self.assignment = assignment self.submission = submission @@ -159,7 +159,7 @@ struct SubmissionCommentList: View { .cancel() ]) } - CommentEditor( + CommentEditorView( text: $comment, shouldShowCommentLibrary: commentLibrary.shouldShow, showCommentLibrary: $showCommentLibrary, diff --git a/Teacher/Teacher/SpeedGrader/Grading/Points/View/SubmissionGrades.swift b/Teacher/Teacher/SpeedGrader/Grading/Points/View/SubmissionGrades.swift index 7323066fa8..ef3027900f 100644 --- a/Teacher/Teacher/SpeedGrader/Grading/Points/View/SubmissionGrades.swift +++ b/Teacher/Teacher/SpeedGrader/Grading/Points/View/SubmissionGrades.swift @@ -126,7 +126,7 @@ struct SubmissionGrades: View { } private func commentEditor() -> some View { - CommentEditor( + CommentEditorView( text: $rubricsViewModel.criterionComment, shouldShowCommentLibrary: false, showCommentLibrary: .constant(false), diff --git a/Teacher/Teacher/SpeedGrader/MainLayout/View/Drawer.swift b/Teacher/Teacher/SpeedGrader/MainLayout/View/DrawerContainer.swift similarity index 98% rename from Teacher/Teacher/SpeedGrader/MainLayout/View/Drawer.swift rename to Teacher/Teacher/SpeedGrader/MainLayout/View/DrawerContainer.swift index 560a841052..138f98b505 100644 --- a/Teacher/Teacher/SpeedGrader/MainLayout/View/Drawer.swift +++ b/Teacher/Teacher/SpeedGrader/MainLayout/View/DrawerContainer.swift @@ -25,7 +25,7 @@ enum DrawerState { } // Place after the main content in a ZStack(alignment: .bottom) -struct Drawer: View { +struct DrawerContainer: View { let content: Content let minHeight: CGFloat let maxHeight: CGFloat diff --git a/Teacher/Teacher/SpeedGrader/MainLayout/View/SimilarityScore.swift b/Teacher/Teacher/SpeedGrader/MainLayout/View/SimilarityScoreView.swift similarity index 98% rename from Teacher/Teacher/SpeedGrader/MainLayout/View/SimilarityScore.swift rename to Teacher/Teacher/SpeedGrader/MainLayout/View/SimilarityScoreView.swift index a143ef8c03..079ba750d7 100644 --- a/Teacher/Teacher/SpeedGrader/MainLayout/View/SimilarityScore.swift +++ b/Teacher/Teacher/SpeedGrader/MainLayout/View/SimilarityScoreView.swift @@ -19,7 +19,7 @@ import SwiftUI import Core -struct SimilarityScore: View { +struct SimilarityScoreView: View { let status: String let score: Double let url: URL? diff --git a/Teacher/Teacher/SpeedGrader/MainLayout/View/SubmissionGrader.swift b/Teacher/Teacher/SpeedGrader/MainLayout/View/SubmissionGrader.swift deleted file mode 100644 index 2a861e6cf4..0000000000 --- a/Teacher/Teacher/SpeedGrader/MainLayout/View/SubmissionGrader.swift +++ /dev/null @@ -1,411 +0,0 @@ -// -// This file is part of Canvas. -// Copyright (C) 2020-present Instructure, Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// - -import Core -import SwiftUI - -struct SubmissionGrader: View { - private enum Layout { - case portrait - case landscape // only on iPads no matter the iPhone screen size - } - - let index: Int - private let assignment: Assignment - private let submission: Submission - private var handleRefresh: (() -> Void)? - - @Environment(\.viewController) var controller - - @ObservedObject var attempts: Store> - @ObservedObject var commentLibrary = SubmissionCommentLibraryViewModel() - - @State var attempt: Int? { - willSet { - let attemptChanged = (selected.attempt != newValue) - - if attemptChanged { - let newAttempt = attempts.first { newValue == $0.attempt } ?? submission - studentAnnotationViewModel = StudentAnnotationSubmissionViewerViewModel(submission: newAttempt) - } - } - } - - @State var drawerState: DrawerState = .min - @State var fileID: String? - @State var showAttempts = false - @State var tab: GraderTab = .grades - @State var showRecorder: MediaCommentType? - /** This is mainly used by `SubmissionCommentList` but since it's re-created on rotation and app backgrounding the entered text is lost. */ - @State var enteredComment: String = "" - /** Used to work around an issue which caused the page to re-load after putting the app into background. See `layoutForWidth()` method for more. */ - @State private var lastPresentedLayout: Layout = .portrait - @State private var studentAnnotationViewModel: StudentAnnotationSubmissionViewerViewModel - @State private var selectedIndex = 0 - @StateObject private var rubricsViewModel: RubricsViewModel - - @AccessibilityFocusState private var focusedTab: GraderTab? - - private var selected: Submission { attempts.first { attempt == $0.attempt } ?? submission } - private var file: File? { - selected.attachments?.first { fileID == $0.id } ?? - selected.attachments?.sorted(by: File.idCompare).first - } - - init( - env: AppEnvironment, - index: Int, - assignment: Assignment, - submission: Submission, - handleRefresh: (() -> Void)? - ) { - self.index = index - self.assignment = assignment - self.submission = submission - attempts = env.subscribe(scope: Scope( - predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [ - NSPredicate(key: #keyPath(Submission.assignmentID), equals: assignment.id), - NSPredicate(key: #keyPath(Submission.userID), equals: submission.userID), - NSPredicate(format: "%K != nil", #keyPath(Submission.submittedAt)) - ]), - orderBy: #keyPath(Submission.attempt) - )) - attempt = submission.attempt - self.handleRefresh = handleRefresh - studentAnnotationViewModel = StudentAnnotationSubmissionViewerViewModel(submission: submission) - _rubricsViewModel = StateObject(wrappedValue: - RubricsViewModel( - assignment: assignment, - submission: submission, - interactor: RubricGradingInteractorLive(assignment: assignment, submission: submission) - ) - ) - } - - var body: some View { - GeometryReader { geometry in - let bottomInset = geometry.safeAreaInsets.bottom - let minHeight = bottomInset + 58 - let maxHeight = bottomInset + geometry.size.height - 64 - // At 1/4 of a screen offset, scale to 90% and round corners to 20 - let delta = abs(geometry.frame(in: .global).minX / max(1, geometry.size.width)) - let scale = interpolate(value: delta, fromMin: 0, fromMax: 0.25, toMin: 1, toMax: 0.9) - let cornerRadius = interpolate(value: delta, fromMin: 0, fromMax: 0.25, toMin: 0, toMax: 20) - - switch layoutForWidth(geometry.size.width) { - case .landscape: - VStack(spacing: 0) { - SubmissionHeader(assignment: assignment, submission: submission) - .accessibility(sortPriority: 2) - Divider() - HStack(spacing: 0) { - VStack(alignment: .leading, spacing: 0) { - attemptToggle - Divider() - ZStack(alignment: .top) { - VStack(spacing: 0) { - SimilarityScore(selected, file: file) - SubmissionViewer( - assignment: assignment, - submission: selected, - fileID: fileID, - studentAnnotationViewModel: studentAnnotationViewModel, - handleRefresh: handleRefresh - ) - } - // Disable submission content interaction in case attempt picker is above it - .accessibilityElement(children: showAttempts ? .ignore : .contain) - .accessibility(hidden: showAttempts) - attemptPicker - } - Spacer().frame(height: bottomInset) - } - .zIndex(1) - .accessibility(sortPriority: 1) - Divider() - VStack(spacing: 0) { - tools(bottomInset: bottomInset, isDrawer: false) - } - .padding(.top, 16) - .frame(width: 375) - } - } - .background(Color.backgroundLightest) - .cornerRadius(cornerRadius) - .scaleEffect(scale) - .edgesIgnoringSafeArea(.bottom) - .onAppear { didChangeLayout(to: .landscape) } - case .portrait: - ZStack(alignment: .bottom) { - VStack(alignment: .leading, spacing: 0) { - SubmissionHeader(assignment: assignment, submission: submission) - attemptToggle - .accessibility(hidden: drawerState == .max) - Divider() - let isSubmissionContentHiddenFromA11y = (drawerState != .min || showAttempts) - ZStack(alignment: .top) { - VStack(spacing: 0) { - SimilarityScore(selected, file: file) - SubmissionViewer( - assignment: assignment, - submission: selected, - fileID: fileID, - studentAnnotationViewModel: studentAnnotationViewModel, - handleRefresh: handleRefresh - ) - } - .accessibilityElement(children: isSubmissionContentHiddenFromA11y ? .ignore : .contain) - .accessibility(hidden: isSubmissionContentHiddenFromA11y) - attemptPicker - } - Spacer().frame(height: drawerState == .min ? minHeight : (minHeight + maxHeight) / 2) - } - Drawer(state: $drawerState, minHeight: minHeight, maxHeight: maxHeight) { - tools(bottomInset: bottomInset, isDrawer: true) - } - } - .background(Color.backgroundLightest) - .cornerRadius(cornerRadius) - .scaleEffect(scale) - .edgesIgnoringSafeArea(.bottom) - .onAppear { didChangeLayout(to: .portrait) } - } - } - .avoidKeyboardArea() - } - - @ViewBuilder - var attemptToggle: some View { - if let first = attempts.first, attempts.count == 1 { - HStack { - Text("Attempt \(attempt ?? 1)", bundle: .teacher).font(.regular14) - Spacer() - Text(first.submittedAt?.dateTimeString ?? "") - .font(.regular14) - .frame(minHeight: 24) - } - .foregroundColor(.textDark) - .padding(EdgeInsets(top: 8, leading: 16, bottom: 4, trailing: 16)) - - } else if let selected = attempts.first(where: { attempt == $0.attempt }) ?? attempts.last { - Button(action: { - showAttempts.toggle() - }, label: { - HStack { - Text("Attempt \(attempt ?? 1)", bundle: .teacher).font(.regular14) - Spacer() - Text(selected.submittedAt?.dateTimeString ?? "") - .font(.regular14) - .frame(minHeight: 24) - Image.arrowOpenDownLine - .resizable() - .frame(width: 14, height: 14) - .rotationEffect(.degrees(showAttempts ? 180 : 0)) - } - .foregroundColor(.textDark) - .padding(EdgeInsets(top: 8, leading: 16, bottom: 4, trailing: 16)) - }) - } - } - - @ViewBuilder - var attemptPicker: some View { - if showAttempts { - VStack(spacing: 0) { - Picker(selection: Binding(get: { selected.attempt }, set: { newValue in - withTransaction(.exclusive()) { - NotificationCenter.default.post(name: .SpeedGraderAttemptPickerChanged, object: newValue) - attempt = newValue - fileID = nil - } - showAttempts = false - }), label: Text(verbatim: "")) { - ForEach(attempts.all, id: \.attempt) { attempt in - Text(attempt.submittedAt?.dateTimeString ?? "") - .tag(Optional(attempt.attempt)) - } - } - .labelsHidden() - .pickerStyle(WheelPickerStyle()) - Divider() - } - .background(Color.backgroundLightest) - } - } - - enum GraderTab: Int, CaseIterable { case grades, comments, files } - - private func segmentedTitles() -> [String] { - let filesString: String! - if selected.type == .online_upload, let count = selected.attachments?.count, count > 0 { - filesString = String(localized: "Files (\(count))", bundle: .teacher) - } else { - filesString = String(localized: "Files", bundle: .teacher) - } - - return [ - String(localized: "Grades", bundle: .teacher), - String(localized: "Comments", bundle: .teacher), - filesString - ] - } - - @ViewBuilder - func tools(bottomInset: CGFloat, isDrawer: Bool) -> some View { - SegmentedPicker( - segmentedTitles(), - selectedIndex: Binding( - get: { selectedIndex }, - set: { newValue in - selectedIndex = newValue ?? 0 - if drawerState == .min { - snapDrawerTo(.mid) - } - let newTab = SubmissionGrader.GraderTab(rawValue: newValue ?? 0)! - withAnimation(.default) { - tab = newTab - } - controller.view.endEditing(true) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - focusedTab = tab - } - } - ), - selectionAlignment: .bottom, - content: { item, _ in - Text(item) - .font(.regular14) - .foregroundColor(.textDark) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .frame(maxWidth: .infinity) - .contentShape(Rectangle()) - } - ) - .onAppear { - selectedIndex = 0 - } - .identifier("SpeedGrader.toolPicker") - Divider() - GeometryReader { geometry in - HStack(spacing: 0) { - let drawerAttempt = Binding(get: { attempt }, set: { - attempt = $0 - fileID = nil - snapDrawerTo(.min) - }) - let drawerFileID = Binding(get: { fileID }, set: { - fileID = $0 - snapDrawerTo(.min) - }) - - let isGradesOnScreen = isGraderTabOnScreen(.grades, isDrawer: isDrawer) - VStack(spacing: 0) { - SubmissionGrades(assignment: assignment, containerHeight: geometry.size.height, submission: submission, rubricsViewModel: rubricsViewModel) - .clipped() - Spacer().frame(height: bottomInset) - } - .frame(width: geometry.size.width, height: geometry.size.height) - .accessibilityElement(children: isGradesOnScreen ? .contain : .ignore) - .accessibility(hidden: !isGradesOnScreen) - .accessibilityFocused($focusedTab, equals: .grades) - - let isCommentsOnScreen = isGraderTabOnScreen(.comments, isDrawer: isDrawer) - VStack(spacing: 0) { - SubmissionCommentList( - assignment: assignment, - submission: submission, - attempts: attempts, - attempt: drawerAttempt, - fileID: drawerFileID, - showRecorder: $showRecorder, - enteredComment: $enteredComment, - commentLibrary: commentLibrary, - focusedTab: _focusedTab - ) - .clipped() - if showRecorder != .video || drawerState == .min { - Spacer().frame(height: bottomInset) - } - } - .frame(width: geometry.size.width, height: geometry.size.height) - .background(Color.backgroundLight) - .accessibilityElement(children: isCommentsOnScreen ? .contain : .ignore) - .accessibility(hidden: !isCommentsOnScreen) - - let isFilesOnScreen = isGraderTabOnScreen(.files, isDrawer: isDrawer) - VStack(spacing: 0) { - SubmissionFileList(submission: selected, fileID: drawerFileID) - .clipped() - Spacer().frame(height: bottomInset) - } - .frame(width: geometry.size.width, height: geometry.size.height) - .accessibilityElement(children: isFilesOnScreen ? .contain : .ignore) - .accessibility(hidden: !isFilesOnScreen) - .accessibilityFocused($focusedTab, equals: .files) - } - .frame(width: geometry.size.width, alignment: .leading) - .background(Color.backgroundLightest) - .offset(x: -CGFloat(tab.rawValue) * geometry.size.width) - } - .clipped() - } - - private func snapDrawerTo(_ state: DrawerState) { - withTransaction(DrawerState.transaction) { - drawerState = state - } - } - - private func isGraderTabOnScreen(_ tab: GraderTab, isDrawer: Bool) -> Bool { - let isTabSelected = (self.tab == tab) - - if isDrawer { - return (drawerState != .min && isTabSelected) - } else { - return isTabSelected - } - } - - private func layoutForWidth(_ width: CGFloat) -> Layout { - // On iPads if the app is backgrounded then it changes the device orientation back and forth causing the UI to re-render and the submission to re-load. - // To overcome this we force the last presented layout in case the app is in the background. - guard UIApplication.shared.applicationState != .background else { - return lastPresentedLayout - } - return width > 834 ? .landscape : .portrait - } - - private func didChangeLayout(to layout: Layout) { - if lastPresentedLayout != layout { - // When the layout changes the keyboard disappears without any system notifications - // on iPads so we simulate one to allow .avoidKeyboardArea() to work correctly. - NotificationCenter.default.post(name: UIApplication.keyboardWillHideNotification, object: nil, userInfo: [:]) - } - lastPresentedLayout = layout - } -} - -private func interpolate(value: CGFloat, fromMin: CGFloat, fromMax: CGFloat, toMin: CGFloat, toMax: CGFloat) -> CGFloat { - let bounded = max(fromMin, min(value, fromMax)) - return (((toMax - toMin) / (fromMax - fromMin)) * (bounded - fromMin)) + toMin -} - -extension NSNotification.Name { - public static var SpeedGraderAttemptPickerChanged = NSNotification.Name("com.instructure.core.speedgrader-attempt-changed") -} diff --git a/Teacher/Teacher/SpeedGrader/MainLayout/View/SubmissionGraderView.swift b/Teacher/Teacher/SpeedGrader/MainLayout/View/SubmissionGraderView.swift new file mode 100644 index 0000000000..b906d98e7b --- /dev/null +++ b/Teacher/Teacher/SpeedGrader/MainLayout/View/SubmissionGraderView.swift @@ -0,0 +1,431 @@ +// +// This file is part of Canvas. +// Copyright (C) 2020-present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +import Core +import SwiftUI +import Combine + +struct SubmissionGraderView: View { + private enum Layout { + case portrait + case landscape // only on iPads no matter the iPhone screen size + } + + let userIndexInSubmissionList: Int + + @Environment(\.viewController) private var controller + + @State private var selectedDrawerTabIndex = 0 + @State private var drawerState: DrawerState = .min + @State private var showAttempts = false + @State private var tab: GraderTab = .grades + @State private var showRecorder: MediaCommentType? + /** Used to work around an issue which caused the page to re-load after putting the app into background. See `layoutForWidth()` method for more. */ + @State private var lastPresentedLayout: Layout = .portrait + @AccessibilityFocusState private var focusedTab: GraderTab? + + @StateObject private var commentLibrary = SubmissionCommentLibraryViewModel() + @StateObject private var rubricsViewModel: RubricsViewModel + @StateObject private var viewModel: SubmissionGraderViewModel + + private var handleRefresh: (() -> Void)? + + init( + env: AppEnvironment, + userIndexInSubmissionList: Int, + viewModel: SubmissionGraderViewModel, + handleRefresh: (() -> Void)? + ) { + self.userIndexInSubmissionList = userIndexInSubmissionList + self._viewModel = StateObject(wrappedValue: viewModel) + self.handleRefresh = handleRefresh + _rubricsViewModel = StateObject(wrappedValue: + RubricsViewModel( + assignment: viewModel.assignment, + submission: viewModel.submission, + interactor: RubricGradingInteractorLive(assignment: viewModel.assignment, submission: viewModel.submission) + ) + ) + } + + var body: some View { + GeometryReader { geometry in + let bottomInset = geometry.safeAreaInsets.bottom + let minHeight = bottomInset + 58 + let maxHeight = bottomInset + geometry.size.height - 64 + // At 1/4 of a screen offset, scale to 90% and round corners to 20 + let delta = abs(geometry.frame(in: .global).minX / max(1, geometry.size.width)) + let scale = interpolate(value: delta, fromMin: 0, fromMax: 0.25, toMin: 1, toMax: 0.9) + let cornerRadius = interpolate(value: delta, fromMin: 0, fromMax: 0.25, toMin: 0, toMax: 20) + + mainLayout( + geometry: geometry, + bottomInset: bottomInset, + minHeight: minHeight, + maxHeight: maxHeight + ) + .background(Color.backgroundLightest) + .cornerRadius(cornerRadius) + .scaleEffect(scale) + .edgesIgnoringSafeArea(.bottom) + } + .avoidKeyboardArea() + } + + @ViewBuilder + private func mainLayout( + geometry: GeometryProxy, + bottomInset: CGFloat, + minHeight: CGFloat, + maxHeight: CGFloat + ) -> some View { + switch layoutForWidth(geometry.size.width) { + case .landscape: + landscapeLayout(bottomInset: bottomInset) + case .portrait: + portraitLayout(minHeight: minHeight, maxHeight: maxHeight, bottomInset: bottomInset) + } + } + + private func landscapeLayout( + bottomInset: CGFloat + ) -> some View { + VStack(spacing: 0) { + SubmissionHeaderView(assignment: viewModel.assignment, submission: viewModel.submission) + .accessibility(sortPriority: 2) + Divider() + HStack(spacing: 0) { + VStack(alignment: .leading, spacing: 0) { + attemptToggle + Divider() + ZStack(alignment: .top) { + VStack(spacing: 0) { + SimilarityScoreView(viewModel.selectedAttempt, file: viewModel.file) + SubmissionViewer( + assignment: viewModel.assignment, + submission: viewModel.selectedAttempt, + fileID: viewModel.fileID, + studentAnnotationViewModel: viewModel.studentAnnotationViewModel, + handleRefresh: handleRefresh + ) + } + // Disable submission content interaction in case attempt picker is above it + .accessibilityElement(children: showAttempts ? .ignore : .contain) + .accessibility(hidden: showAttempts) + attemptPicker + } + Spacer().frame(height: bottomInset) + } + .zIndex(1) + .accessibility(sortPriority: 1) + Divider() + VStack(spacing: 0) { + tools(bottomInset: bottomInset, isDrawer: false) + } + .padding(.top, 16) + .frame(width: 375) + } + } + .onAppear { didChangeLayout(to: .landscape) } + } + + private func portraitLayout( + minHeight: CGFloat, + maxHeight: CGFloat, + bottomInset: CGFloat + ) -> some View { + ZStack(alignment: .bottom) { + VStack(alignment: .leading, spacing: 0) { + SubmissionHeaderView(assignment: viewModel.assignment, submission: viewModel.submission) + attemptToggle + .accessibility(hidden: drawerState == .max) + Divider() + let isSubmissionContentHiddenFromA11y = (drawerState != .min || showAttempts) + ZStack(alignment: .top) { + VStack(spacing: 0) { + SimilarityScoreView(viewModel.selectedAttempt, file: viewModel.file) + SubmissionViewer( + assignment: viewModel.assignment, + submission: viewModel.selectedAttempt, + fileID: viewModel.fileID, + studentAnnotationViewModel: viewModel.studentAnnotationViewModel, + handleRefresh: handleRefresh + ) + } + .accessibilityElement(children: isSubmissionContentHiddenFromA11y ? .ignore : .contain) + .accessibility(hidden: isSubmissionContentHiddenFromA11y) + attemptPicker + } + Spacer().frame(height: drawerState == .min ? minHeight : (minHeight + maxHeight) / 2) + } + DrawerContainer(state: $drawerState, minHeight: minHeight, maxHeight: maxHeight) { + tools(bottomInset: bottomInset, isDrawer: true) + } + } + .onAppear { didChangeLayout(to: .portrait) } + } + + @ViewBuilder + private var attemptToggle: some View { + if viewModel.hasSubmissions { + Button { + showAttempts.toggle() + } label: { + HStack { + Text("Attempt \(viewModel.selectedAttemptIndex)", bundle: .teacher) + Spacer() + Text(viewModel.selectedAttempt.submittedAt?.dateTimeString ?? "") + .frame(minHeight: 24) + + if !viewModel.isSingleSubmission { + Image.arrowOpenDownLine + .resizable() + .frame(width: 14, height: 14) + .rotationEffect(.degrees(showAttempts ? 180 : 0)) + } + } + .font(.regular14) + .foregroundColor(.textDark) + .padding(EdgeInsets(top: 8, leading: 16, bottom: 4, trailing: 16)) + } + .disabled(viewModel.isSingleSubmission) + } + } + + @ViewBuilder + private var attemptPicker: some View { + if showAttempts { + VStack(spacing: 0) { + let binding = Binding( + get: { + viewModel.selectedAttemptIndex + }, + set: { newAttemptIndex in + withTransaction(.exclusive()) { + viewModel.didSelectNewAttempt(attemptIndex: newAttemptIndex) + } + showAttempts = false + } + ) + Picker(selection: binding, label: Text(verbatim: "")) { + ForEach(viewModel.attempts, id: \.attempt) { attempt in + Text(attempt.submittedAt?.dateTimeString ?? "") + .tag(Optional(attempt.attempt)) + } + } + .labelsHidden() + .pickerStyle(WheelPickerStyle()) + Divider() + } + .background(Color.backgroundLightest) + } + } + + // MARK: - Drawer + + enum GraderTab: Int, CaseIterable { case grades, comments, files } + + private var segmentedTitles: [String] { + [ + String(localized: "Grades", bundle: .teacher), + String(localized: "Comments", bundle: .teacher), + viewModel.fileTabTitle + ] + } + + @ViewBuilder + private func tools(bottomInset: CGFloat, isDrawer: Bool) -> some View { + SegmentedPicker( + segmentedTitles, + selectedIndex: Binding( + get: { selectedDrawerTabIndex }, + set: { newValue in + selectedDrawerTabIndex = newValue ?? 0 + if drawerState == .min { + snapDrawerTo(.mid) + } + let newTab = SubmissionGraderView.GraderTab(rawValue: newValue ?? 0)! + withAnimation(.default) { + tab = newTab + } + controller.view.endEditing(true) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + focusedTab = tab + } + } + ), + selectionAlignment: .bottom, + content: { item, _ in + Text(item) + .font(.regular14) + .foregroundColor(.textDark) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + } + ) + .identifier("SpeedGrader.toolPicker") + Divider() + GeometryReader { geometry in + HStack(spacing: 0) { + let drawerFileID = Binding( + get: { + viewModel.fileID + }, + set: { + viewModel.didSelectFile(fileID: $0) + snapDrawerTo(.min) + } + ) + + gradesTab(bottomInset: bottomInset, isDrawer: isDrawer, geometry: geometry) + commentsTab(bottomInset: bottomInset, isDrawer: isDrawer, fileID: drawerFileID, geometry: geometry) + filesTab(bottomInset: bottomInset, isDrawer: isDrawer, fileID: drawerFileID, geometry: geometry) + } + .frame(width: geometry.size.width, alignment: .leading) + .background(Color.backgroundLightest) + .offset(x: -CGFloat(tab.rawValue) * geometry.size.width) + } + .clipped() + } + + private func snapDrawerTo(_ state: DrawerState) { + withTransaction(DrawerState.transaction) { + drawerState = state + } + } + + private func isGraderTabOnScreen(_ tab: GraderTab, isDrawer: Bool) -> Bool { + let isTabSelected = (self.tab == tab) + + if isDrawer { + return (drawerState != .min && isTabSelected) + } else { + return isTabSelected + } + } + + // MARK: - Tab Contents + + @ViewBuilder + private func gradesTab( + bottomInset: CGFloat, + isDrawer: Bool, + geometry: GeometryProxy + ) -> some View { + let isGradesOnScreen = isGraderTabOnScreen(.grades, isDrawer: isDrawer) + VStack(spacing: 0) { + SubmissionGrades( + assignment: viewModel.assignment, + containerHeight: geometry.size.height, + submission: viewModel.submission, + rubricsViewModel: rubricsViewModel + ) + .clipped() + Spacer().frame(height: bottomInset) + } + .frame(width: geometry.size.width, height: geometry.size.height) + .accessibilityElement(children: isGradesOnScreen ? .contain : .ignore) + .accessibility(hidden: !isGradesOnScreen) + .accessibilityFocused($focusedTab, equals: .grades) + } + + @ViewBuilder + private func commentsTab( + bottomInset: CGFloat, + isDrawer: Bool, + fileID: Binding, + geometry: GeometryProxy + ) -> some View { + let drawerAttempt = Binding( + get: { + viewModel.selectedAttemptIndex + }, set: { + viewModel.didSelectNewAttempt(attemptIndex: $0) + snapDrawerTo(.min) + } + ) + let isCommentsOnScreen = isGraderTabOnScreen(.comments, isDrawer: isDrawer) + VStack(spacing: 0) { + SubmissionCommentListView( + assignment: viewModel.assignment, + submission: viewModel.submission, + attempts: viewModel.attempts, + attempt: drawerAttempt, + fileID: fileID, + showRecorder: $showRecorder, + enteredComment: $viewModel.enteredComment, + commentLibrary: commentLibrary, + focusedTab: _focusedTab + ) + .clipped() + if showRecorder != .video || drawerState == .min { + Spacer().frame(height: bottomInset) + } + } + .frame(width: geometry.size.width, height: geometry.size.height) + .background(Color.backgroundLight) + .accessibilityElement(children: isCommentsOnScreen ? .contain : .ignore) + .accessibility(hidden: !isCommentsOnScreen) + } + + @ViewBuilder + private func filesTab( + bottomInset: CGFloat, + isDrawer: Bool, + fileID: Binding, + geometry: GeometryProxy + ) -> some View { + let isFilesOnScreen = isGraderTabOnScreen(.files, isDrawer: isDrawer) + VStack(spacing: 0) { + SubmissionFileList(submission: viewModel.selectedAttempt, fileID: fileID) + .clipped() + Spacer().frame(height: bottomInset) + } + .frame(width: geometry.size.width, height: geometry.size.height) + .accessibilityElement(children: isFilesOnScreen ? .contain : .ignore) + .accessibility(hidden: !isFilesOnScreen) + .accessibilityFocused($focusedTab, equals: .files) + } + + // MARK: - Rotation + + private func layoutForWidth(_ width: CGFloat) -> Layout { + // On iPads if the app is backgrounded then it changes the device orientation back and forth causing the UI to re-render and the submission to re-load. + // To overcome this we force the last presented layout in case the app is in the background. + guard UIApplication.shared.applicationState != .background else { + return lastPresentedLayout + } + return width > 834 ? .landscape : .portrait + } + + private func didChangeLayout(to layout: Layout) { + if lastPresentedLayout != layout { + // When the layout changes the keyboard disappears without any system notifications + // on iPads so we simulate one to allow .avoidKeyboardArea() to work correctly. + NotificationCenter.default.post(name: UIApplication.keyboardWillHideNotification, object: nil, userInfo: [:]) + } + lastPresentedLayout = layout + } +} + +private func interpolate(value: CGFloat, fromMin: CGFloat, fromMax: CGFloat, toMin: CGFloat, toMax: CGFloat) -> CGFloat { + let bounded = max(fromMin, min(value, fromMax)) + return (((toMax - toMin) / (fromMax - fromMin)) * (bounded - fromMin)) + toMin +} diff --git a/Teacher/Teacher/SpeedGrader/MainLayout/View/SubmissionHeader.swift b/Teacher/Teacher/SpeedGrader/MainLayout/View/SubmissionHeaderView.swift similarity index 99% rename from Teacher/Teacher/SpeedGrader/MainLayout/View/SubmissionHeader.swift rename to Teacher/Teacher/SpeedGrader/MainLayout/View/SubmissionHeaderView.swift index e98a6a1851..5c7e662f37 100644 --- a/Teacher/Teacher/SpeedGrader/MainLayout/View/SubmissionHeader.swift +++ b/Teacher/Teacher/SpeedGrader/MainLayout/View/SubmissionHeaderView.swift @@ -19,7 +19,7 @@ import SwiftUI import Core -struct SubmissionHeader: View { +struct SubmissionHeaderView: View { let assignment: Assignment let submission: Submission diff --git a/Teacher/Teacher/SpeedGrader/MainLayout/ViewModel/SubmissionGraderViewModel.swift b/Teacher/Teacher/SpeedGrader/MainLayout/ViewModel/SubmissionGraderViewModel.swift new file mode 100644 index 0000000000..8b842e9652 --- /dev/null +++ b/Teacher/Teacher/SpeedGrader/MainLayout/ViewModel/SubmissionGraderViewModel.swift @@ -0,0 +1,114 @@ +// +// This file is part of Canvas. +// Copyright (C) 2025-present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +import Combine +import Core +import CoreData + +class SubmissionGraderViewModel: ObservableObject { + + // MARK: - Outputs + + @Published private(set) var selectedAttemptIndex: Int + @Published private(set) var selectedAttempt: Submission + @Published private(set) var studentAnnotationViewModel: StudentAnnotationSubmissionViewerViewModel + @Published private(set) var attempts: [Submission] = [] + @Published private(set) var hasSubmissions = false + @Published private(set) var isSingleSubmission = false + @Published private(set) var file: File? + @Published private(set) var fileID: String? + @Published private(set) var fileTabTitle: String = "" + let assignment: Assignment + let submission: Submission + + // MARK: - Inputs + + /** This is mainly used by `SubmissionCommentList` but since it's re-created on rotation and app backgrounding the entered text is lost. */ + @Published var enteredComment: String = "" + + private var subscriptions = Set() + + init(assignment: Assignment, submission: Submission) { + self.assignment = assignment + self.submission = submission + selectedAttempt = submission + selectedAttemptIndex = submission.attempt + studentAnnotationViewModel = StudentAnnotationSubmissionViewerViewModel(submission: submission) + observeAttemptChangesInDatabase() + didSelectNewAttempt(attemptIndex: submission.attempt) + } + + func didSelectNewAttempt(attemptIndex: Int) { + NotificationCenter.default.post(name: .SpeedGraderAttemptPickerChanged, object: attemptIndex) + selectedAttemptIndex = attemptIndex + selectedAttempt = attempts.first { selectedAttemptIndex == $0.attempt } ?? submission + fileTabTitle = { + if selectedAttempt.type == .online_upload, let count = selectedAttempt.attachments?.count, count > 0 { + return String(localized: "Files (\(count))", bundle: .teacher) + } else { + return String(localized: "Files", bundle: .teacher) + } + }() + studentAnnotationViewModel = StudentAnnotationSubmissionViewerViewModel(submission: selectedAttempt) + didSelectFile(fileID: nil) + } + + func didSelectFile(fileID: String?) { + self.fileID = fileID + updateSelectedFile() + } + + private func updateSelectedFile() { + file = selectedAttempt.attachments?.first { fileID == $0.id } ?? + selectedAttempt.attachments?.sorted(by: File.idCompare).first + } + + private func observeAttemptChangesInDatabase() { + let scope = Scope( + predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [ + NSPredicate(key: #keyPath(Submission.assignmentID), equals: assignment.id), + NSPredicate(key: #keyPath(Submission.userID), equals: submission.userID), + NSPredicate(format: "%K != nil", #keyPath(Submission.submittedAt)) + ]), + orderBy: #keyPath(Submission.attempt) + ) + let useCase = LocalUseCase(scope: scope) + ReactiveStore(useCase: useCase) + .getEntities(keepObservingDatabaseChanges: true) + .replaceError(with: []) + .receive(on: DispatchQueue.main) + .sink { [weak self] attempts in + guard let self else { return } + self.attempts = attempts + hasSubmissions = attempts.lastAttemptIndex > 0 + isSingleSubmission = attempts.lastAttemptIndex == 1 + } + .store(in: &subscriptions) + } +} + +extension [Submission] { + + fileprivate var lastAttemptIndex: Int { + last?.attempt ?? 0 + } +} + +extension NSNotification.Name { + public static var SpeedGraderAttemptPickerChanged = NSNotification.Name("com.instructure.core.speedgrader-attempt-changed") +} diff --git a/Teacher/Teacher/SpeedGrader/StudentPager/View/SpeedGraderViewController.swift b/Teacher/Teacher/SpeedGrader/StudentPager/View/SpeedGraderViewController.swift index d62fe0d2bf..7aa160aa42 100644 --- a/Teacher/Teacher/SpeedGrader/StudentPager/View/SpeedGraderViewController.swift +++ b/Teacher/Teacher/SpeedGrader/StudentPager/View/SpeedGraderViewController.swift @@ -22,7 +22,7 @@ import UIKit import Core class SpeedGraderViewController: ScreenViewTrackableViewController, PagesViewControllerDataSource { - typealias Page = CoreHostingController + typealias Page = CoreHostingController var env: AppEnvironment = .defaultValue @@ -138,7 +138,7 @@ class SpeedGraderViewController: ScreenViewTrackableViewController, PagesViewCon private func updatePages() { for page in pages.children.compactMap({ $0 as? Page }) { - if let grader = grader(for: page.rootView.content.index) { + if let grader = grader(for: page.rootView.content.userIndexInSubmissionList) { page.rootView.content = grader } } @@ -147,11 +147,11 @@ class SpeedGraderViewController: ScreenViewTrackableViewController, PagesViewCon // MARK: - PagesViewControllerDataSource func pagesViewController(_ pages: PagesViewController, pageBefore page: UIViewController) -> UIViewController? { - (page as? Page).flatMap { controller(for: $0.rootView.content.index - 1) } + (page as? Page).flatMap { controller(for: $0.rootView.content.userIndexInSubmissionList - 1) } } func pagesViewController(_ pages: PagesViewController, pageAfter page: UIViewController) -> UIViewController? { - (page as? Page).flatMap { controller(for: $0.rootView.content.index + 1) } + (page as? Page).flatMap { controller(for: $0.rootView.content.userIndexInSubmissionList + 1) } } func controller(for index: Int) -> UIViewController? { @@ -160,18 +160,20 @@ class SpeedGraderViewController: ScreenViewTrackableViewController, PagesViewCon return controller } - func grader(for index: Int) -> SubmissionGrader? { + func grader(for index: Int) -> SubmissionGraderView? { guard let data = interactor.data, index >= 0, index < data.submissions.count else { return nil } - return SubmissionGrader( + return SubmissionGraderView( env: env, - index: index, - assignment: data.assignment, - submission: data.submissions[index], + userIndexInSubmissionList: index, + viewModel: SubmissionGraderViewModel( + assignment: data.assignment, + submission: data.submissions[index] + ), handleRefresh: { [weak self] in self?.interactor.refreshSubmission(forUserId: data.submissions[index].userID) } diff --git a/Teacher/Teacher/SpeedGrader/SubmissionRenderer/ViewModel/StudentAnnotationSubmissionViewerViewModel.swift b/Teacher/Teacher/SpeedGrader/SubmissionRenderer/ViewModel/StudentAnnotationSubmissionViewerViewModel.swift index 7dca3024d7..f34d74c946 100644 --- a/Teacher/Teacher/SpeedGrader/SubmissionRenderer/ViewModel/StudentAnnotationSubmissionViewerViewModel.swift +++ b/Teacher/Teacher/SpeedGrader/SubmissionRenderer/ViewModel/StudentAnnotationSubmissionViewerViewModel.swift @@ -21,12 +21,14 @@ import SwiftUI class StudentAnnotationSubmissionViewerViewModel: ObservableObject { @Published public private(set) var session: Result? + public let submissionId: String private var isInitialLoadStarted = false private let request: CanvaDocsSessionRequest public init(submission: Submission) { - self.request = CanvaDocsSessionRequest(submissionId: submission.id, attempt: "\(submission.attempt)") + submissionId = submission.id + request = CanvaDocsSessionRequest(submissionId: submission.id, attempt: "\(submission.attempt)") } public func viewDidAppear() { diff --git a/Teacher/TeacherTests/SpeedGrader/MainLayout/View/SubmissionHeaderTests.swift b/Teacher/TeacherTests/SpeedGrader/MainLayout/View/SubmissionHeaderViewTests.swift similarity index 86% rename from Teacher/TeacherTests/SpeedGrader/MainLayout/View/SubmissionHeaderTests.swift rename to Teacher/TeacherTests/SpeedGrader/MainLayout/View/SubmissionHeaderViewTests.swift index c0df1c8e0d..f6ef6f5c0e 100644 --- a/Teacher/TeacherTests/SpeedGrader/MainLayout/View/SubmissionHeaderTests.swift +++ b/Teacher/TeacherTests/SpeedGrader/MainLayout/View/SubmissionHeaderViewTests.swift @@ -25,7 +25,7 @@ class SubmissionHeaderTests: TeacherTestCase { func testGroupSubmissionCheck() { let submission = Submission(context: databaseClient) let assignment = Assignment(context: databaseClient) - let testee = SubmissionHeader(assignment: assignment, submission: submission) + let testee = SubmissionHeaderView(assignment: assignment, submission: submission) assignment.gradedIndividually = false submission.groupID = "TestGroupID" @@ -36,7 +36,7 @@ class SubmissionHeaderTests: TeacherTestCase { func testGroupName() { let submission = Submission(context: databaseClient) let assignment = Assignment(context: databaseClient) - let testee = SubmissionHeader(assignment: assignment, submission: submission) + let testee = SubmissionHeaderView(assignment: assignment, submission: submission) assignment.gradedIndividually = false submission.groupName = "TestGroup Name" @@ -49,7 +49,7 @@ class SubmissionHeaderTests: TeacherTestCase { func testRouteToGroupSubmitter() { let submission = Submission(context: databaseClient) let assignment = Assignment(context: databaseClient) - let testee = SubmissionHeader(assignment: assignment, submission: submission) + let testee = SubmissionHeaderView(assignment: assignment, submission: submission) assignment.gradedIndividually = false assignment.courseID = "testCourseID" @@ -61,7 +61,7 @@ class SubmissionHeaderTests: TeacherTestCase { func testRouteToIndividialInGroupSubmission() { let submission = Submission(context: databaseClient) let assignment = Assignment(context: databaseClient) - let testee = SubmissionHeader(assignment: assignment, submission: submission) + let testee = SubmissionHeaderView(assignment: assignment, submission: submission) assignment.gradedIndividually = true assignment.courseID = "testCourseID"