diff --git a/apps/flutter_parent/lib/l10n/res/intl_sv.arb b/apps/flutter_parent/lib/l10n/res/intl_sv.arb index cf3e28412c..4d3578f905 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_sv.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_sv.arb @@ -244,7 +244,7 @@ "type": "text", "placeholders": {} }, - "domainSearchHelpBody": "Försök med att söka efter namnet på skolan eller distrikten du vill ansluta till, t.ex. “Allmänna skolan” eller “Skolor i Skåne”. Du kan även ange en Canvas-domän direkt, t.ex. “smith.instructure.com.”\n\nMer information om hur du kan hitta din institutions Canvas-konto finns på {canvasGuides} eller kontakta {canvasSupport} eller din skola för att få hjälp.", + "domainSearchHelpBody": "Försök med att söka efter namnet på skolan eller distrikten du vill ansluta till, t.ex. “Allmänna skolan” eller “Skolor i Skåne”. Du kan även ange en Canvas-domän direkt, t.ex. “smith.instructure.com.”\n\nMer information om hur du kan hitta din lärosätes Canvas-konto finns på {canvasGuides} eller kontakta {canvasSupport} eller din skola för att få hjälp.", "@domainSearchHelpBody": { "description": "The body text shown in the help dialog on the domain search screen", "type": "text", @@ -511,7 +511,7 @@ "howMany": {} } }, - "Download": "Ladda ned", + "Download": "Ladda ner", "@Download": { "description": "Label for the button that will begin downloading a file", "type": "text", @@ -965,7 +965,7 @@ "type": "text", "placeholders": {} }, - "Institution Announcement": "Institutionsmeddelande", + "Institution Announcement": "Meddelande från lärosätet", "@Institution Announcement": { "description": "Title for alerts when there is an institution announcement", "type": "text", @@ -1081,7 +1081,7 @@ "type": "text", "placeholders": {} }, - "Incomplete": "ofullständig", + "Incomplete": "Inte färdig", "@Incomplete": { "description": "Grading status for an assignment marked as incomplete", "type": "text", @@ -1154,7 +1154,7 @@ "type": "text", "placeholders": {} }, - "Institution Announcements": "Institutionsannonseringar", + "Institution Announcements": "Meddelande från lärosätet", "@Institution Announcements": { "type": "text", "placeholders": {} @@ -2025,7 +2025,7 @@ "type": "text", "placeholders": {} }, - "Interactions on this page are limited by your institution.": "Interaktioner på den här sidan har begränsats av din institution.", + "Interactions on this page are limited by your institution.": "Interaktioner på den här sidan har begränsats av ditt lärosäte.", "@Interactions on this page are limited by your institution.": { "description": "Message describing how the webview has limited access due to an instution setting", "type": "text", @@ -2128,7 +2128,7 @@ "type": "text", "placeholders": {} }, - "We are unable to display this link, it may belong to an institution you currently aren't logged in to.": "Det går inte att visa den här länken. Den kan tillhöra en institution du för närvarande inte är inloggad på.", + "We are unable to display this link, it may belong to an institution you currently aren't logged in to.": "Det går inte att visa den här länken. Den kan tillhöra ett lärosäte du för närvarande inte är inloggad på.", "@We are unable to display this link, it may belong to an institution you currently aren't logged in to.": { "description": "Description for error page shown when clicking a link", "type": "text", diff --git a/apps/flutter_parent/lib/l10n/res/intl_sv_instk12.arb b/apps/flutter_parent/lib/l10n/res/intl_sv_instk12.arb index 881ba02e32..2733f68557 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_sv_instk12.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_sv_instk12.arb @@ -511,7 +511,7 @@ "howMany": {} } }, - "Download": "Ladda ned", + "Download": "Ladda ner", "@Download": { "description": "Label for the button that will begin downloading a file", "type": "text", diff --git a/apps/student/build.gradle b/apps/student/build.gradle index 966a9784d0..eb7d68d8d1 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -26,7 +26,7 @@ apply from: '../../gradle/coverage.gradle' apply plugin: 'com.squareup.sqldelight' apply plugin: 'dagger.hilt.android.plugin' -def updatePriority = 0 +def updatePriority = 2 def coverageEnabled = project.hasProperty('coverage') if (coverageEnabled) { @@ -59,8 +59,8 @@ android { applicationId "com.instructure.candroid" minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 228 - versionName = '6.12.0' + versionCode = 230 + versionName = '6.14.0' vectorDrawables.useSupportLibrary = true multiDexEnabled = true diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt index dab48f2c11..1e82f452bb 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt @@ -89,7 +89,6 @@ class ElementaryDashboardInteractionTest : StudentTest() { // We have to add this delay to be sure that the remote config is already fetched before we want to override remote config values. Thread.sleep(3000) RemoteConfigPrefs.putString(RemoteConfigParam.K5_DESIGN.rc_name, "true") - FeatureFlagPrefs.showInProgressK5Tabs = true val data = MockCanvas.init( studentCount = 1, diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GradesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GradesInteractionTest.kt new file mode 100644 index 0000000000..4a384575d3 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GradesInteractionTest.kt @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.ui.interaction + +import com.instructure.canvas.espresso.containsTextCaseInsensitive +import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockCanvas.addCourseWithEnrollment +import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.utils.RemoteConfigParam +import com.instructure.canvasapi2.utils.RemoteConfigPrefs +import com.instructure.espresso.page.getStringFromResource +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.student.R +import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.tokenLoginElementary +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Test + +@HiltAndroidTest +class GradesInteractionTest : StudentTest() { + + override fun displaysPageObjects() = Unit + + @Test + @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testShowGrades() { + val data = createMockData(courseCount = 3) + goToGrades(data) + + gradesPage.assertPageObjects() + + data.courses.forEach { + gradesPage.assertCourseShownWithGrades(it.value.name, "B+") + } + } + + @Test + @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testRefresh() { + val data = createMockData(courseCount = 3) + goToGrades(data) + + gradesPage.assertPageObjects() + + data.courses.forEach { + gradesPage.assertCourseShownWithGrades(it.value.name, "B+") + } + + val newCourse = data.addCourseWithEnrollment(data.students[0], Enrollment.EnrollmentType.Student, 50.0) + + gradesPage.refresh() + + gradesPage.assertCourseShownWithGrades(newCourse.name, "50%") + } + + @Test + @TestMetaData(Priority.P2, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testEmptyView() { + val data = createMockData(homeroomCourseCount = 1) + goToGrades(data) + + gradesPage.assertEmptyViewVisible() + gradesPage.assertRecyclerViewNotVisible() + } + + @Test + @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testOpenCourseGrades() { + val data = createMockData(courseCount = 3) + goToGrades(data) + + val course = data.courses.values.first() + + gradesPage.clickGradeRow(course.name) + courseGradesPage.assertPageObjects() + courseGradesPage.assertTotalGrade(containsTextCaseInsensitive("B+")) + } + + @Test + @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testChangeGradingPeriod() { + val data = createMockData(courseCount = 3, withGradingPeriods = true) + goToGrades(data) + + gradesPage.assertSelectedGradingPeriod(gradesPage.getStringFromResource(R.string.currentGradingPeriod)) + gradesPage.clickGradingPeriodSelector() + + val gradingPeriod = data.courseGradingPeriods.values.first().first() + gradesPage.selectGradingPeriod(gradingPeriod.title!!) + gradesPage.assertSelectedGradingPeriod(gradingPeriod.title!!) + } + + private fun createMockData( + courseCount: Int = 0, + withGradingPeriods: Boolean = false, + homeroomCourseCount: Int = 0): MockCanvas { + + // We have to add this delay to be sure that the remote config is already fetched before we want to override remote config values. + Thread.sleep(3000) + RemoteConfigPrefs.putString(RemoteConfigParam.K5_DESIGN.rc_name, "true") + + return MockCanvas.init( + studentCount = 1, + courseCount = courseCount, + withGradingPeriods = withGradingPeriods, + homeroomCourseCount = homeroomCourseCount) + } + + private fun goToGrades(data: MockCanvas) { + val student = data.students[0] + val token = data.tokenFor(student)!! + tokenLoginElementary(data.domain, token, student) + elementaryDashboardPage.waitForRender() + elementaryDashboardPage.selectGradesTab() + } +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt index 4f3160188b..117e1276aa 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt @@ -205,7 +205,7 @@ class HomeroomInteractionTest : StudentTest() { homeroomPage.assertPageObjects() - homeroomPage.openCourseAnnouncment(courseAnnouncement.title!!) + homeroomPage.openCourseAnnouncement(courseAnnouncement.title!!) discussionDetailsPage.assertPageObjects() discussionDetailsPage.assertTitleText(courseAnnouncement.title!!) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt new file mode 100644 index 0000000000..2d6d90176b --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.ui.interaction + +import com.instructure.canvas.espresso.mockCanvas.* +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.utils.RemoteConfigParam +import com.instructure.canvasapi2.utils.RemoteConfigPrefs +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.util.FeatureFlagPrefs +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Test + +@HiltAndroidTest +class ResourcesInteractionTest : StudentTest() { + + override fun displaysPageObjects() = Unit + + @Test + @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testImportantLinksAndActionItemsShowUpInResourcesScreen() { + val data = createMockDataWithHomeroomCourse(courseCount = 2) + + val homeroomCourse = data.courses.values.first { it.homeroomCourse } + val courseWithSyllabus = homeroomCourse.copy(syllabusBody = "Important links content") + data.courses[homeroomCourse.id] = courseWithSyllabus + + val nonHomeroomCourses = data.courses.values.filter { !it.homeroomCourse } + nonHomeroomCourses.forEach { + data.addLTITool("Google Drive", "http://google.com", it, 1234L) + data.addLTITool("Media Gallery", "http://instructure.com", it, 12345L) + } + + goToResources(data) + + resourcesPage.assertPageObjects() + resourcesPage.assertImportantLinksDisplayed(courseWithSyllabus.syllabusBody!!) + + resourcesPage.assertStudentApplicationsHeaderDisplayed() + resourcesPage.assertLtiToolDisplayed("Google Drive") + resourcesPage.assertLtiToolDisplayed("Media Gallery") + + val teacher = data.teachers[0] + resourcesPage.assertStaffInfoHeaderDisplayed() + resourcesPage.assertStaffDisplayed(teacher.shortName!!) + } + + @Test + @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testImportantLinksForTwoCourses() { + val data = createMockDataWithHomeroomCourse(courseCount = 2) + + val homeroomCourse = data.courses.values.first { it.homeroomCourse } + val courseWithSyllabus = homeroomCourse.copy(syllabusBody = "Important links content") + data.courses[homeroomCourse.id] = courseWithSyllabus + + val homeroomCourse2 = data.addCourseWithEnrollment(data.students[0], Enrollment.EnrollmentType.Student, isHomeroom = true) + data.addEnrollment(data.teachers[0], homeroomCourse, Enrollment.EnrollmentType.Teacher) + + val courseWithSyllabus2 = homeroomCourse2.copy(syllabusBody = "Important links 2") + data.courses[homeroomCourse2.id] = courseWithSyllabus2 + + goToResources(data) + + resourcesPage.assertPageObjects() + + // We only assert the course names, because can't differentiate between the two WebViews. + resourcesPage.assertCourseNameDisplayed(courseWithSyllabus.name) + resourcesPage.assertCourseNameDisplayed(courseWithSyllabus2.name) + } + + @Test + @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testOnlyActionItemsShowIfSyllabusIsEmpty() { + val data = createMockDataWithHomeroomCourse(courseCount = 2) + + val homeroomCourse = data.courses.values.first { it.homeroomCourse } + val courseWithSyllabus = homeroomCourse.copy(syllabusBody = "") + data.courses[homeroomCourse.id] = courseWithSyllabus + + val nonHomeroomCourses = data.courses.values.filter { !it.homeroomCourse } + nonHomeroomCourses.forEach { + data.addLTITool("Google Drive", "http://google.com", it, 1234L) + data.addLTITool("Media Gallery", "http://instructure.com", it, 12345L) + } + + goToResources(data) + + resourcesPage.assertImportantLinksNotDisplayed() + + resourcesPage.assertStudentApplicationsHeaderDisplayed() + resourcesPage.assertLtiToolDisplayed("Google Drive") + resourcesPage.assertLtiToolDisplayed("Media Gallery") + + val teacher = data.teachers[0] + resourcesPage.assertStaffInfoHeaderDisplayed() + resourcesPage.assertStaffDisplayed(teacher.shortName!!) + } + + @Test + @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testOnlyLtiToolsShowIfNoHomeroomCourse() { + val data = createMockDataWithHomeroomCourse(courseCount = 2, homeroomCourseCount = 0) + + val nonHomeroomCourses = data.courses.values.filter { !it.homeroomCourse } + nonHomeroomCourses.forEach { + data.addLTITool("Google Drive", "http://google.com", it, 1234L) + data.addLTITool("Media Gallery", "http://instructure.com", it, 12345L) + } + + goToResources(data) + + resourcesPage.assertImportantLinksNotDisplayed() + + resourcesPage.assertStudentApplicationsHeaderDisplayed() + resourcesPage.assertLtiToolDisplayed("Google Drive") + resourcesPage.assertLtiToolDisplayed("Media Gallery") + + resourcesPage.assertStaffInfoNotDisplayed() + } + + @Test + @TestMetaData(Priority.P2, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testEmptyState() { + val data = createMockDataWithHomeroomCourse(courseCount = 2, homeroomCourseCount = 0) + + goToResources(data) + + resourcesPage.assertImportantLinksNotDisplayed() + resourcesPage.assertStudentApplicationsNotDisplayed() + resourcesPage.assertStaffInfoNotDisplayed() + resourcesPage.assertEmptyViewDisplayed() + } + + @Test + @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testRefresh() { + val data = createMockDataWithHomeroomCourse(courseCount = 2, homeroomCourseCount = 0) + + goToResources(data) + + resourcesPage.assertEmptyViewDisplayed() + + val homeroomCourse = data.addCourseWithEnrollment(data.students[0], Enrollment.EnrollmentType.Student, isHomeroom = true) + data.addEnrollment(data.teachers[0], homeroomCourse, Enrollment.EnrollmentType.Teacher) + + val courseWithSyllabus = homeroomCourse.copy(syllabusBody = "Important links content") + data.courses[homeroomCourse.id] = courseWithSyllabus + + val nonHomeroomCourses = data.courses.values.filter { !it.homeroomCourse } + nonHomeroomCourses.forEach { + data.addLTITool("Google Drive", "http://google.com", it, 1234L) + data.addLTITool("Media Gallery", "http://instructure.com", it, 12345L) + } + + resourcesPage.refresh() + + resourcesPage.assertPageObjects() + resourcesPage.assertImportantLinksDisplayed(courseWithSyllabus.syllabusBody!!) + + resourcesPage.assertStudentApplicationsHeaderDisplayed() + resourcesPage.assertLtiToolDisplayed("Google Drive") + resourcesPage.assertLtiToolDisplayed("Media Gallery") + + val teacher = data.teachers[0] + resourcesPage.assertStaffInfoHeaderDisplayed() + resourcesPage.assertStaffDisplayed(teacher.shortName!!) + } + + @Test + @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testOpenLtiToolShowsCourseSelector() { + val data = createMockDataWithHomeroomCourse(courseCount = 2) + + val homeroomCourse = data.courses.values.first { it.homeroomCourse } + val courseWithSyllabus = homeroomCourse.copy(syllabusBody = "Important links content") + data.courses[homeroomCourse.id] = courseWithSyllabus + + val nonHomeroomCourses = data.courses.values.filter { !it.homeroomCourse } + nonHomeroomCourses.forEach { + data.addLTITool("Google Drive", "http://google.com", it, 1234L) + data.addLTITool("Media Gallery", "http://instructure.com", it, 12345L) + } + + goToResources(data) + + resourcesPage.openLtiApp("Google Drive") + nonHomeroomCourses.forEach { + resourcesPage.assertCourseShown(it.name) + } + } + + @Test + @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testOpenComposeMessageScreen() { + val data = createMockDataWithHomeroomCourse(courseCount = 2) + + val homeroomCourse = data.courses.values.first { it.homeroomCourse } + val courseWithSyllabus = homeroomCourse.copy(syllabusBody = "Important links content") + data.courses[homeroomCourse.id] = courseWithSyllabus + + val nonHomeroomCourses = data.courses.values.filter { !it.homeroomCourse } + nonHomeroomCourses.forEach { + data.addLTITool("Google Drive", "http://google.com", it, 1234L) + data.addLTITool("Media Gallery", "http://instructure.com", it, 12345L) + } + + goToResources(data) + resourcesPage.openComposeMessage(data.teachers[0].shortName!!) + + newMessagePage.assertToolbarTitleNewMessage() + newMessagePage.assertCourseSelectorNotShown() + newMessagePage.assertRecipientsNotShown() + newMessagePage.assertSendIndividualMessagesNotShown() + newMessagePage.assertSubjectViewShown() + newMessagePage.assertMessageViewShown() + } + + private fun createMockDataWithHomeroomCourse( + courseCount: Int = 0, + pastCourseCount: Int = 0, + favoriteCourseCount: Int = 0, + announcementCount: Int = 0, + homeroomCourseCount: Int = 1): MockCanvas { + + // We have to add this delay to be sure that the remote config is already fetched before we want to override remote config values. + Thread.sleep(3000) + RemoteConfigPrefs.putString(RemoteConfigParam.K5_DESIGN.rc_name, "true") + + return MockCanvas.init( + studentCount = 1, + teacherCount = 1, + courseCount = courseCount, + pastCourseCount = pastCourseCount, + favoriteCourseCount = favoriteCourseCount, + accountNotificationCount = announcementCount, + homeroomCourseCount = homeroomCourseCount) + } + + private fun goToResources(data: MockCanvas) { + val student = data.students[0] + val token = data.tokenFor(student)!! + tokenLoginElementary(data.domain, token, student) + elementaryDashboardPage.waitForRender() + elementaryDashboardPage.selectResourcesTab() + } +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt new file mode 100644 index 0000000000..ac411ea948 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.ui.interaction + +import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockCanvas.addAssignment +import com.instructure.canvas.espresso.mockCanvas.addTodo +import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.utils.RemoteConfigParam +import com.instructure.canvasapi2.utils.RemoteConfigPrefs +import com.instructure.canvasapi2.utils.toApiString +import com.instructure.espresso.page.getStringFromResource +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.pandautils.utils.date.DateTimeProvider +import com.instructure.student.R +import com.instructure.student.ui.utils.FakeDateTimeProvider +import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.tokenLoginElementary +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Before +import org.junit.Test +import java.util.* +import javax.inject.Inject + +@HiltAndroidTest +class ScheduleInteractionTest : StudentTest() { + + @Inject + lateinit var dateTimeProvider: DateTimeProvider + + override fun displaysPageObjects() = Unit + + @Before + fun setUp() { + if (!this::dateTimeProvider.isInitialized) { + hiltRule.inject() + } + } + + @Test + @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testShowCorrectHeaderItems() { + setDate(2021, Calendar.AUGUST, 11) + val data = createMockData(courseCount = 1) + goToSchedule(data) + + schedulePage.assertPageObjects() + schedulePage.assertDayHeaderShown("August 08", "Sunday", 0) + schedulePage.assertDayHeaderShown("August 09", "Monday", 2) + schedulePage.assertNoScheduleItemDisplayed() + + schedulePage.assertDayHeaderShown("August 10", schedulePage.getStringFromResource(R.string.yesterday), 4) + schedulePage.assertDayHeaderShown("August 11", schedulePage.getStringFromResource(R.string.today), 6) + schedulePage.assertNoScheduleItemDisplayed() + + schedulePage.assertDayHeaderShown("August 12", schedulePage.getStringFromResource(R.string.tomorrow), 8) + schedulePage.assertDayHeaderShown("August 13", "Friday", 10) + schedulePage.assertNoScheduleItemDisplayed() + + schedulePage.assertDayHeaderShown("August 14", "Saturday", 12) + schedulePage.assertNoScheduleItemDisplayed() + } + + @Test + @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testShowScheduledAssignments() { + setDate(2021, Calendar.AUGUST, 11) + val data = createMockData(courseCount = 1) + + val courses = data.courses.values.filter { !it.homeroomCourse } + courses[0].name = "Course 1" + + val currentDate = dateTimeProvider.getCalendar().time.toApiString() + val assignment1 = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate, name = "Assignment 1") + + goToSchedule(data) + schedulePage.scrollToPosition(10) + schedulePage.assertCourseHeaderDisplayed(courses[0].name) + schedulePage.assertScheduleItemDisplayed(assignment1.name!!) + } + + @Test + @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testShowMissingAssignments() { + setDate(2021, Calendar.AUGUST, 11) + val data = createMockData(courseCount = 1) + + val courses = data.courses.values.filter { !it.homeroomCourse } + + val currentDate = dateTimeProvider.getCalendar().time.toApiString() + val assignment1 = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate) + + goToSchedule(data) + schedulePage.scrollToPosition(12) + schedulePage.assertMissingItemDisplayed(assignment1.name!!, courses[0].name, "10 pts") + } + + @Test + @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testShowToDoEvents() { + setDate(2021, Calendar.AUGUST, 11) + val data = createMockData(courseCount = 1) + + val todo = data.addTodo("To Do event", data.students[0].id, date = dateTimeProvider.getCalendar().time) + val todo2 = data.addTodo("Calendar event", data.students[0].id, date = dateTimeProvider.getCalendar().time) + + goToSchedule(data) + schedulePage.scrollToPosition(8) + schedulePage.assertCourseHeaderDisplayed(schedulePage.getStringFromResource(R.string.schedule_todo_title)) + schedulePage.assertScheduleItemDisplayed(todo.plannable.title) + schedulePage.assertScheduleItemDisplayed(todo2.plannable.title) + } + + @Test + @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testRefresh() { + setDate(2021, Calendar.AUGUST, 11) + val data = createMockData(courseCount = 1) + + val courses = data.courses.values.filter { !it.homeroomCourse } + + goToSchedule(data) + + // Check that we don't have any elements initially + schedulePage.assertNoScheduleItemDisplayed() + schedulePage.scrollToPosition(8) + schedulePage.assertNoScheduleItemDisplayed() + + val currentDate = dateTimeProvider.getCalendar().time.toApiString() + val assignment1 = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate) + val assignment2 = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate) + + schedulePage.scrollToPosition(0) + schedulePage.refresh() + + // Check that refresh was successful + schedulePage.scrollToPosition(7) + schedulePage.assertCourseHeaderDisplayed(courses[0].name) + schedulePage.assertScheduleItemDisplayed(assignment1.name!!) + schedulePage.assertScheduleItemDisplayed(assignment2.name!!) + } + + @Test + @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testGoBack2Weeks() { + setDate(2021, Calendar.AUGUST, 11) + val data = createMockData(courseCount = 1) + + goToSchedule(data) + + schedulePage.assertDayHeaderShown("August 08", "Sunday", 0) + schedulePage.assertDayHeaderShown("August 09", "Monday", 2) + + schedulePage.previousWeekButtonClick() + schedulePage.swipeRight() + + schedulePage.assertDayHeaderShown("July 25", "Sunday", 0, recyclerViewMatcherText = "July 25") + schedulePage.assertDayHeaderShown("July 26", "Monday", 2, recyclerViewMatcherText = "July 25") + schedulePage.assertDayHeaderShown("July 27", "Tuesday", 4, recyclerViewMatcherText = "July 26") + schedulePage.assertDayHeaderShown("July 28", "Wednesday", 6, recyclerViewMatcherText = "July 27") + schedulePage.assertDayHeaderShown("July 29", "Thursday", 8, recyclerViewMatcherText = "July 28") + schedulePage.assertDayHeaderShown("July 30", "Friday", 10, recyclerViewMatcherText = "July 29") + schedulePage.assertDayHeaderShown("July 31", "Saturday", 12, recyclerViewMatcherText = "July 30") + } + + @Test + @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testGoForward2Weeks() { + setDate(2021, Calendar.AUGUST, 11) + val data = createMockData(courseCount = 1) + + goToSchedule(data) + + schedulePage.assertDayHeaderShown("August 08", "Sunday", 0) + schedulePage.assertDayHeaderShown("August 09", "Monday", 2) + + schedulePage.nextWeekButtonClick() + schedulePage.swipeLeft() + + schedulePage.assertDayHeaderShown("August 22", "Sunday", 0, recyclerViewMatcherText = "August 22") + schedulePage.assertDayHeaderShown("August 23", "Monday", 2, recyclerViewMatcherText = "August 22") + schedulePage.assertDayHeaderShown("August 24", "Tuesday", 4, recyclerViewMatcherText = "August 23") + schedulePage.assertDayHeaderShown("August 25", "Wednesday", 6, recyclerViewMatcherText = "August 24") + schedulePage.assertDayHeaderShown("August 26", "Thursday", 8, recyclerViewMatcherText = "August 25") + schedulePage.assertDayHeaderShown("August 27", "Friday", 10, recyclerViewMatcherText = "August 26") + schedulePage.assertDayHeaderShown("August 28", "Saturday", 12, recyclerViewMatcherText = "August 27") + } + + @Test + @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testOpenAssignment() { + setDate(2021, Calendar.AUGUST, 11) + val data = createMockData(courseCount = 1) + + val courses = data.courses.values.filter { !it.homeroomCourse } + courses[0].name = "Course 1" + + val currentDate = dateTimeProvider.getCalendar().time.toApiString() + val assignment = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate, name = "Assignment 1") + + goToSchedule(data) + schedulePage.scrollToPosition(9) + schedulePage.clickScheduleItem(assignment.name!!) + + assignmentDetailsPage.assertPageObjects() + assignmentDetailsPage.verifyAssignmentDetails(assignment) + } + + @Test + @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testOpenCourse() { + setDate(2021, Calendar.AUGUST, 11) + val data = createMockData(courseCount = 1) + + val courses = data.courses.values.filter { !it.homeroomCourse } + + val currentDate = dateTimeProvider.getCalendar().time.toApiString() + data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate) + + goToSchedule(data) + schedulePage.scrollToPosition(8) + schedulePage.clickCourseHeader(courses[0].name) + + courseBrowserPage.assertPageObjects() + courseBrowserPage.assertTitleCorrect(courses[0]) + } + + @Test + @TestMetaData(Priority.P2, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION) + fun testMarkAsDone() { + setDate(2021, Calendar.AUGUST, 11) + val data = createMockData(courseCount = 1) + + data.addTodo("To Do event", data.students[0].id, date = dateTimeProvider.getCalendar().time) + + goToSchedule(data) + schedulePage.scrollToPosition(8) + + schedulePage.assertMarkedAsDoneNotShown() + + schedulePage.clickDoneCheckbox() + schedulePage.assertMarkedAsDoneShown() + } + + private fun createMockData( + courseCount: Int = 0, + withGradingPeriods: Boolean = false, + homeroomCourseCount: Int = 0): MockCanvas { + + // We have to add this delay to be sure that the remote config is already fetched before we want to override remote config values. + Thread.sleep(3000) + RemoteConfigPrefs.putString(RemoteConfigParam.K5_DESIGN.rc_name, "true") + + return MockCanvas.init( + studentCount = 1, + courseCount = courseCount, + withGradingPeriods = withGradingPeriods, + homeroomCourseCount = homeroomCourseCount) + } + + private fun goToSchedule(data: MockCanvas) { + val student = data.students[0] + val token = data.tokenFor(student)!! + tokenLoginElementary(data.domain, token, student) + elementaryDashboardPage.waitForRender() + elementaryDashboardPage.selectScheduleTab() + } + + private fun setDate(year: Int, month: Int, dayOfMonth: Int) { + val cal = Calendar.getInstance() + cal.set(Calendar.YEAR, year) + cal.set(Calendar.MONTH, month) + cal.set(Calendar.DAY_OF_MONTH, dayOfMonth) + (dateTimeProvider as FakeDateTimeProvider).fakeTimeInMillis = cal.timeInMillis + } +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt index 0034c69b61..9fd5f912e7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt @@ -115,6 +115,7 @@ class SettingsInteractionTest : StudentTest() { fun testPairObserver_refreshCode() { setUpAndSignIn() + ApiPrefs.canGeneratePairingCode = true dashboardPage.launchSettingsPage() settingsPage.launchPairObserverPage() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentListPage.kt index 405413750c..10461bb1be 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentListPage.kt @@ -17,7 +17,6 @@ package com.instructure.student.ui.pages import android.view.View -import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.* @@ -28,6 +27,8 @@ import com.instructure.dataseeding.model.AssignmentApiModel import com.instructure.dataseeding.model.QuizApiModel import com.instructure.espresso.* import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.waitForView import com.instructure.espresso.page.waitForViewWithText import com.instructure.student.R import org.hamcrest.Matcher @@ -74,7 +75,7 @@ class AssignmentListPage : BasePage(pageResId = R.id.assignmentListPage) { private fun assertHasAssignmentCommon(assignmentName: String, assignmentDueAt: String?, expectedGrade: String? = null) { waitForMatcherWithRefreshes(withText(assignmentName)) - waitForViewWithText(assignmentName).assertDisplayed() + waitForView(allOf(withText(assignmentName), isDescendantOfA(withId(R.id.assignmentListPage)))).assertDisplayed() // Check that either the assignment due date is present, or "No Due Date" is displayed if(assignmentDueAt != null) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GradesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GradesPage.kt index 576ad03129..f13e38b928 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GradesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GradesPage.kt @@ -16,8 +16,55 @@ */ package com.instructure.student.ui.pages -import com.instructure.espresso.page.BasePage +import com.instructure.espresso.* +import com.instructure.espresso.page.* import com.instructure.student.R class GradesPage : BasePage(R.id.gradesPage) { + + private val swipeRefreshLayout by OnViewWithId(R.id.gradesRefreshLayout) + private val gradesRecyclerView by OnViewWithId(R.id.gradesRecyclerView) + private val emptyView by OnViewWithId(R.id.gradesEmptyView, autoAssert = false) + + fun assertCourseShownWithGrades(courseName: String, grade: String) { + val courseNameMatcher = withId(R.id.gradesCourseNameText) + withText(courseName) + val gradeMatcher = withId(R.id.scoreText) + withText(grade) + + onView(withId(R.id.gradeRow) + withDescendant(courseNameMatcher) + withDescendant(gradeMatcher)) + .scrollTo() + .assertDisplayed() + } + + fun refresh() { + swipeRefreshLayout.swipeDown() + } + + fun assertEmptyViewVisible() { + emptyView.assertDisplayed() + } + + fun assertRecyclerViewNotVisible() { + gradesRecyclerView.assertNotDisplayed() + } + + fun clickGradeRow(courseName: String) { + onView(withId(R.id.gradesCourseNameText) + withText(courseName)) + .scrollTo() + .click() + } + + fun clickGradingPeriodSelector() { + onView(withId(R.id.gradingPeriodSelector)) + .click() + } + + fun selectGradingPeriod(gradingPeriodName: String) { + onView(withText(gradingPeriodName)) + .click() + } + + fun assertSelectedGradingPeriod(gradingPeriodName: String) { + onView(withId(R.id.gradingPeriodSelector) + withText(gradingPeriodName)) + .assertDisplayed() + } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HomeroomPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HomeroomPage.kt index b9abb82543..437f780447 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HomeroomPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HomeroomPage.kt @@ -97,9 +97,9 @@ class HomeroomPage : BasePage(R.id.homeroomPage) { .click() } - fun openCourseAnnouncment(announcementText: String) { + fun openCourseAnnouncement(announcementText: String) { + swipeRefreshLayout.swipeUp() onView(withId(R.id.announcementText) + withText(announcementText)) - .scrollTo() .click() } @@ -116,8 +116,8 @@ class HomeroomPage : BasePage(R.id.homeroomPage) { } fun openAssignments(todoText: String) { + swipeRefreshLayout.swipeUp() onView(withId(R.id.todoText) + withText(todoText)) - .scrollTo() .click() } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/NewMessagePage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/NewMessagePage.kt index 6819469c76..7d146add29 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/NewMessagePage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/NewMessagePage.kt @@ -22,23 +22,19 @@ import androidx.test.espresso.Espresso import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.ViewAssertion import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.* -import com.instructure.canvas.espresso.withCustomConstraints import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.User import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.CourseApiModel import com.instructure.dataseeding.model.GroupApiModel import com.instructure.espresso.* -import com.instructure.espresso.page.BasePage -import com.instructure.espresso.page.onView -import com.instructure.espresso.page.onViewWithId +import com.instructure.espresso.page.* import com.instructure.student.R import junit.framework.Assert.assertTrue -import org.hamcrest.Matchers.* +import org.hamcrest.Matchers.allOf class NewMessagePage : BasePage() { @@ -172,6 +168,30 @@ class NewMessagePage : BasePage() { setMessage(message) Espresso.closeSoftKeyboard() } + + fun assertToolbarTitleNewMessage() { + onView(withId(R.id.toolbar) + withDescendant(withText(R.string.newMessage))).assertDisplayed() + } + + fun assertCourseSelectorNotShown() { + coursesSpinner.assertNotDisplayed() + } + + fun assertRecipientsNotShown() { + onViewWithId(R.id.recipientWrapper).assertNotDisplayed() + } + + fun assertSendIndividualMessagesNotShown() { + sendIndividualMessageSwitch.assertNotDisplayed() + } + + fun assertSubjectViewShown() { + onViewWithId(R.id.editSubject).assertDisplayed() + } + + fun assertMessageViewShown() { + onViewWithId(R.id.message) + } } /** Custom ViewAssertion to make sure that a TextBox is empty */ diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ResourcesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ResourcesPage.kt index b6e52f050d..302f348b35 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ResourcesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ResourcesPage.kt @@ -16,8 +16,84 @@ */ package com.instructure.student.ui.pages -import com.instructure.espresso.page.BasePage +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.web.assertion.WebViewAssertions +import androidx.test.espresso.web.sugar.Web +import androidx.test.espresso.web.webdriver.DriverAtoms +import androidx.test.espresso.web.webdriver.Locator +import com.instructure.espresso.* +import com.instructure.espresso.page.* import com.instructure.student.R +import org.hamcrest.Matchers class ResourcesPage : BasePage(R.id.resourcesPage) { + + private val swipeRefreshLayout by OnViewWithId(R.id.resourcesSwipeRefreshLayout) + private val importantLinksTitle by OnViewWithId(R.id.importantLinksTitle, autoAssert = false) + private val importantLinksContainer by OnViewWithId(R.id.importantLinksContainer) + private val coursesRecyclerView by OnViewWithId(R.id.actionItemsRecyclerView) + + fun assertImportantLinksDisplayed(content: String) { + importantLinksTitle.assertDisplayed() + Web.onWebView() + .withElement(DriverAtoms.findElement(Locator.TAG_NAME, "html")) + .check(WebViewAssertions.webMatches(DriverAtoms.getText(), Matchers.comparesEqualTo(content))) + } + + fun assertCourseNameDisplayed(courseName: String) { + onView(withId(R.id.importantLinksCourseName) + withText(courseName)).assertDisplayed() + } + + fun assertStudentApplicationsHeaderDisplayed() { + onView(withText(R.string.studentApplications)).assertDisplayed() + } + + fun assertLtiToolDisplayed(name: String) { + onView(withId(R.id.ltiAppCardView) + withDescendant(withText(name))).assertDisplayed() + } + + fun assertStaffInfoHeaderDisplayed() { + onView(withText(R.string.staffContactInfo)).assertDisplayed() + } + + fun assertStaffDisplayed(name: String) { + onView(withId(R.id.contactInfoLayout) + withDescendant(withText(name))).assertDisplayed() + } + + fun assertImportantLinksNotDisplayed() { + importantLinksTitle.assertNotDisplayed() + importantLinksContainer.check(ViewAssertions.matches(ViewMatchers.hasChildCount(0))) + } + + fun assertStaffInfoNotDisplayed() { + onView(withText(R.string.staffContactInfo)).check(ViewAssertions.doesNotExist()) + onView(withId(R.id.contactInfoLayout)).check(ViewAssertions.doesNotExist()) + } + + fun assertStudentApplicationsNotDisplayed() { + onView(withText(R.string.studentApplications)).check(ViewAssertions.doesNotExist()) + onView(withId(R.id.ltiAppCardView)).check(ViewAssertions.doesNotExist()) + } + + fun assertEmptyViewDisplayed() { + onViewWithId(R.id.resourcesEmptyView).assertDisplayed() + onViewWithText(R.string.resourcesEmptyMessage).assertDisplayed() + } + + fun refresh() { + swipeRefreshLayout.swipeDown() + } + + fun openLtiApp(name: String) { + onView(withId(R.id.ltiAppCardView) + withDescendant(withText(name))).click() + } + + fun assertCourseShown(courseName: String) { + onView(withText(courseName)) + } + + fun openComposeMessage(teacherName: String) { + onView(withId(R.id.contactInfoLayout) + withDescendant(withText(teacherName))).click() + } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt index 2f1d0f4f60..f56c4b93e4 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt @@ -16,8 +16,97 @@ */ package com.instructure.student.ui.pages -import com.instructure.espresso.page.BasePage +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.contrib.RecyclerViewActions +import com.instructure.espresso.* +import com.instructure.espresso.page.* +import com.instructure.pandautils.binding.BindableViewHolder import com.instructure.student.R class SchedulePage : BasePage(R.id.schedulePage) { + + private val pager by OnViewWithId(R.id.schedulePager) + private val previousWeekButton by OnViewWithId(R.id.previousWeekButton) + private val nextWeekButton by OnViewWithId(R.id.nextWeekButton) + private val recyclerView by OnViewWithId(R.id.scheduleRecyclerView) + private val swipeRefreshLayout by OnViewWithId(R.id.scheduleSwipeRefreshLayout) + + fun assertDayHeaderShown(dateText: String, dayText: String, position: Int, recyclerViewMatcherText: String? = null) { + val dateTextMatcher = withId(R.id.dateText) + withText(dateText) + val dayTextMatcher = withId(R.id.dayText) + withText(dayText) + + val todayHeaderMatcher = withId(R.id.scheduleHeaderLayout) + withDescendant(dateTextMatcher) + withDescendant(dayTextMatcher) + if (recyclerViewMatcherText != null) { + val recyclerViewInteraction = onView(withId(R.id.scheduleRecyclerView) + withDescendant(withText(recyclerViewMatcherText))) + recyclerViewInteraction.perform(RecyclerViewActions.scrollToPosition(position)) + } else { + recyclerView.perform(RecyclerViewActions.scrollToPosition(position)) + } + waitForView(todayHeaderMatcher).assertDisplayed() + } + + fun assertNoScheduleItemDisplayed() { + onView(withId(R.id.scheduleCourseItemLayout)).check(ViewAssertions.doesNotExist()) + } + + fun scrollToPosition(position: Int) { + recyclerView.perform(RecyclerViewActions.scrollToPosition(position)) + } + + fun assertCourseHeaderDisplayed(courseName: String) { + onView(withId(R.id.scheduleCourseHeaderText) + withText(courseName)).assertDisplayed() + } + + fun assertScheduleItemDisplayed(scheduleItemName: String) { + onView(withAncestor(R.id.plannerItems) + withText(scheduleItemName)).assertDisplayed() + } + + fun assertMissingItemDisplayed(itemName: String, courseName: String, pointsPossible: String) { + val titleMatcher = withId(R.id.title) + withText(itemName) + val courseNameMatcher = withId(R.id.courseName) + withText(courseName) + val pointsPossibleMatcher = withId(R.id.points) + withText(pointsPossible) + + onView(withId(R.id.missingItemLayout) + withDescendant(titleMatcher) + withDescendant(courseNameMatcher) + withDescendant(pointsPossibleMatcher)) + .assertDisplayed() + } + + fun refresh() { + swipeRefreshLayout.swipeDown() + } + + fun previousWeekButtonClick() { + previousWeekButton.click() + } + + fun swipeRight() { + pager.swipeRight() + } + + fun nextWeekButtonClick() { + nextWeekButton.click() + } + + fun swipeLeft() { + pager.swipeLeft() + } + + fun clickCourseHeader(courseName: String) { + onView(withId(R.id.scheduleCourseHeaderText) + withText(courseName)).click() + } + + fun clickScheduleItem(name: String) { + onView(withAncestor(R.id.plannerItems) + withText(name)).click() + } + + fun clickDoneCheckbox() { + onView(withId(R.id.checkbox)).click() + } + + fun assertMarkedAsDoneShown() { + onViewWithText(R.string.schedule_marked_as_done).assertDisplayed() + } + + fun assertMarkedAsDoneNotShown() { + onViewWithText(R.string.schedule_marked_as_done).assertNotDisplayed() + } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/TestDateTimeModule.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/TestDateTimeModule.kt new file mode 100644 index 0000000000..7e18e00d08 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/TestDateTimeModule.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.ui.utils + +import com.instructure.pandautils.di.DateTimeModule +import com.instructure.pandautils.utils.date.DateTimeProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn +import java.util.* +import javax.inject.Singleton + +@Module +@TestInstallIn(components = [SingletonComponent::class], replaces = [DateTimeModule::class]) +class TestDateTimeModule { + + @Provides + @Singleton + fun provideDateTimeProvider(): DateTimeProvider { + return FakeDateTimeProvider() + } +} + +class FakeDateTimeProvider : DateTimeProvider { + + var fakeTimeInMillis: Long = Calendar.getInstance().timeInMillis + + override fun getCalendar(): Calendar { + return Calendar.getInstance().apply { + timeInMillis = fakeTimeInMillis + } + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt index 41a934c58a..d39e0c2b4b 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt @@ -75,6 +75,10 @@ abstract class CallbackActivity : ParentActivity(), InboxFragment.OnUnreadCountI } } + val termsOfService = awaitApi { UserManager.getTermsOfService(it, true) } + ApiPrefs.canGeneratePairingCode = termsOfService.selfRegistrationType == SelfRegistration.ALL + || termsOfService.selfRegistrationType == SelfRegistration.OBSERVER + // Grab colors if (ColorKeeper.hasPreviouslySynced) { UserManager.getColors(userColorsCallback, true) diff --git a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt index d0fc9b66cd..b5264e3f96 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt @@ -99,8 +99,10 @@ import kotlinx.coroutines.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import java.util.* import javax.inject.Inject +private const val BOTTOM_NAV_SCREEN = "bottomNavScreen" @AndroidEntryPoint @Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE") @@ -126,6 +128,8 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. private var mDrawerToggle: ActionBarDrawerToggle? = null private var colorOverlayJob: Job? = null + private val bottomNavScreensStack: Deque = ArrayDeque() + override fun contentResId(): Int = R.layout.activity_navigation private val isDrawerOpen: Boolean @@ -194,7 +198,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. /* Update nav bar visibility to show for specific 'root' fragments. Also show the nav bar when there is only one fragment on the backstack, which commonly occurs with non-root fragments when routing from external sources. */ - val visible = it::class.java in navigationBehavior.bottomNavBarFragments || supportFragmentManager.backStackEntryCount <= 1 + val visible = isBottomNavFragment(it) || supportFragmentManager.backStackEntryCount <= 1 bottomBar.setVisible(visible) bottomBarDivider.setVisible(visible) } @@ -225,6 +229,8 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. MasqueradeHelper.startMasquerading(masqueradingUserId, ApiPrefs.domain, NavigationActivity::class.java) } + bottomBar.inflateMenu(navigationBehavior.bottomBarMenu) + supportFragmentManager.addOnBackStackChangedListener(onBackStackChangedListener) if (savedInstanceState == null) { @@ -314,8 +320,8 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. override fun loadLandingPage(clearBackStack: Boolean) { if (clearBackStack) clearBackStack(navigationBehavior.homeFragmentClass) - val homeRoute = navigationBehavior.createHomeFragmentRoute(ApiPrefs.user) - addFragment(navigationBehavior.createHomeFragment(homeRoute), homeRoute) + selectBottomNavFragment(navigationBehavior.homeFragmentClass) + bottomNavScreensStack.clear() if (intent.extras?.containsKey(AppShortcutManager.APP_SHORTCUT_PLACEMENT) == true) { // Launch to the app shortcut placement @@ -329,26 +335,15 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. val route = BookmarksFragment.makeRoute(ApiPrefs.user) addFragment(BookmarksFragment.newInstance(route) { RouteMatcher.routeUrl(this, it.url!!) }, route) } - AppShortcutManager.APP_SHORTCUT_CALENDAR -> { - val route = CalendarFragment.makeRoute() - addFragment(CalendarFragment.newInstance(route), route) - } - AppShortcutManager.APP_SHORTCUT_TODO -> { - val route = ToDoListFragment.makeRoute(ApiPrefs.user!!) - addFragment(ToDoListFragment.newInstance(route), route) - } - AppShortcutManager.APP_SHORTCUT_NOTIFICATIONS -> { - val route = NotificationListFragment.makeRoute(ApiPrefs.user!!) - addFragment(NotificationListFragment.newInstance(route), route) - } + AppShortcutManager.APP_SHORTCUT_CALENDAR -> selectBottomNavFragment(CalendarFragment::class.java) + AppShortcutManager.APP_SHORTCUT_TODO -> selectBottomNavFragment(ToDoListFragment::class.java) + AppShortcutManager.APP_SHORTCUT_NOTIFICATIONS -> selectBottomNavFragment(NotificationListFragment::class.java) AppShortcutManager.APP_SHORTCUT_INBOX -> { if (ApiPrefs.isStudentView) { // Inbox not available in Student View - val route = NothingToSeeHereFragment.makeRoute() - addFragment(NothingToSeeHereFragment.newInstance(), route) + selectBottomNavFragment(NothingToSeeHereFragment::class.java) } else { - val route = InboxFragment.makeRoute() - addFragment(InboxFragment.newInstance(route), route) + selectBottomNavFragment(InboxFragment::class.java) } } } @@ -451,10 +446,14 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. Logger.e("Error getting version: " + e) } - toolbar.setNavigationIcon(R.drawable.ic_hamburger) - toolbar.navigationContentDescription = getString(R.string.navigation_drawer_open) - toolbar.setNavigationOnClickListener { - openNavigationDrawer() + if (isBottomNavFragment(fragment)) { + toolbar.setNavigationIcon(R.drawable.ic_hamburger) + toolbar.navigationContentDescription = getString(R.string.navigation_drawer_open) + toolbar.setNavigationOnClickListener { + openNavigationDrawer() + } + } else { + toolbar.setupAsBackButton(fragment) } drawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START) @@ -545,24 +544,15 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. private val bottomBarItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item: MenuItem -> when (item.itemId) { - R.id.bottomNavigationHome -> handleRoute(Route(navigationBehavior.homeFragmentClass, ApiPrefs.user)) - R.id.bottomNavigationCalendar -> handleRoute(CalendarFragment.makeRoute()) - R.id.bottomNavigationToDo -> { - val route = ToDoListFragment.makeRoute(ApiPrefs.user!!) - addFragment(ToDoListFragment.newInstance(route), route) - } - R.id.bottomNavigationNotifications ->{ - val route = NotificationListFragment.makeRoute(ApiPrefs.user!!) - addFragment(NotificationListFragment.newInstance(route), route) - } + R.id.bottomNavigationHome -> selectBottomNavFragment(navigationBehavior.homeFragmentClass) + R.id.bottomNavigationCalendar -> selectBottomNavFragment(CalendarFragment::class.java) + R.id.bottomNavigationToDo -> selectBottomNavFragment(ToDoListFragment::class.java) + R.id.bottomNavigationNotifications -> selectBottomNavFragment(NotificationListFragment::class.java) R.id.bottomNavigationInbox -> { if (ApiPrefs.isStudentView) { - // Inbox not available in Student View - val route = NothingToSeeHereFragment.makeRoute() - addFragment(NothingToSeeHereFragment.newInstance(), route) + selectBottomNavFragment(NothingToSeeHereFragment::class.java) } else { - val route = InboxFragment.makeRoute() - addFragment(InboxFragment.newInstance(route), route) + selectBottomNavFragment(InboxFragment::class.java) } } } @@ -586,24 +576,15 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. if(!abortReselect) { when (item.itemId) { - R.id.bottomNavigationHome -> handleRoute(Route(navigationBehavior.homeFragmentClass, ApiPrefs.user)) - R.id.bottomNavigationCalendar -> handleRoute(CalendarFragment.makeRoute()) - R.id.bottomNavigationToDo -> { - val route = ToDoListFragment.makeRoute(ApiPrefs.user!!) - addFragment(ToDoListFragment.newInstance(route), route) - } - R.id.bottomNavigationNotifications -> { - val route = NotificationListFragment.makeRoute(ApiPrefs.user!!) - addFragment(NotificationListFragment.newInstance(route), route) - } + R.id.bottomNavigationHome -> selectBottomNavFragment(navigationBehavior.homeFragmentClass) + R.id.bottomNavigationCalendar -> selectBottomNavFragment(CalendarFragment::class.java) + R.id.bottomNavigationToDo -> selectBottomNavFragment(ToDoListFragment::class.java) + R.id.bottomNavigationNotifications -> selectBottomNavFragment(NotificationListFragment::class.java) R.id.bottomNavigationInbox -> { if (ApiPrefs.isStudentView) { - // Inbox not available in Student View - val route = NothingToSeeHereFragment.makeRoute() - addFragment(NothingToSeeHereFragment.newInstance(), route) + selectBottomNavFragment(NothingToSeeHereFragment::class.java) } else { - val route = InboxFragment.makeRoute() - addFragment(InboxFragment.newInstance(route), route) + selectBottomNavFragment(InboxFragment::class.java) } } } @@ -679,19 +660,6 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. } addBookmark() return true - } else if (item.itemId == android.R.id.home) { - //if we hit the x while we're on a detail fragment, we always want to close the top fragment - //and not have it trigger an actual "back press" - val topFragment = topFragment - if (supportFragmentManager.backStackEntryCount > 0) { - if (topFragment != null) { - supportFragmentManager.beginTransaction().remove(topFragment).commit() - } - super.onBackPressed() - } else if (topFragment == null) { - super.onBackPressed() - } - return true } return super.onOptionsItemSelected(item) @@ -787,31 +755,72 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. } private fun addFragment(fragment: Fragment?, route: Route) { + if (RouteType.DIALOG == route.routeType && fragment is DialogFragment && isTablet) { + val ft = supportFragmentManager.beginTransaction() + ft.addToBackStack(fragment::class.java.name) + fragment.show(ft, fragment::class.java.name) + } else { + if (fragment != null && fragment::class.java.name in getBottomNavFragmentNames() && isBottomNavFragment(currentFragment)) { + selectBottomNavFragment(fragment::class.java) + } else { + addFullScreenFragment(fragment) + } + } + } + + private fun selectBottomNavFragment(fragmentClass: Class) { + val selectedFragment = supportFragmentManager.findFragmentByTag(fragmentClass.name) + + if (selectedFragment == null) { + val fragment = createBottomNavFragment(fragmentClass.name) + val newArguments = if (fragment?.arguments != null) fragment.requireArguments() else Bundle() + newArguments.putBoolean(BOTTOM_NAV_SCREEN, true) + fragment?.arguments = newArguments + addFullScreenFragment(fragment) + } else { + showHiddenFragment(selectedFragment) + } + + bottomNavScreensStack.remove(fragmentClass.name) + bottomNavScreensStack.push(fragmentClass.name) + } + + private fun addFullScreenFragment(fragment: Fragment?) { if (fragment == null) { - Logger.e("NavigationActivity:addFragment() - Could not route null Fragment.") + Logger.e("NavigationActivity:addFullScreenFragment() - Could not route null Fragment.") return } val ft = supportFragmentManager.beginTransaction() + ft.setCustomAnimations(R.anim.fade_in_quick, R.anim.fade_out_quick) + currentFragment?.let { ft.hide(it) } + ft.add(R.id.fullscreen, fragment, fragment::class.java.name) + ft.addToBackStack(fragment::class.java.name) + ft.commitAllowingStateLoss() + } - if (RouteType.DIALOG == route.routeType && fragment is DialogFragment && isTablet) { - ft.addToBackStack(fragment::class.java.name) - fragment.show(ft, fragment::class.java.name) - } else { - ft.setCustomAnimations(R.anim.fade_in_quick, R.anim.fade_out_quick) - currentFragment?.let { ft.hide(it) } - ft.add(R.id.fullscreen, fragment, fragment::class.java.name) - ft.addToBackStack(fragment::class.java.name) - ft.commitAllowingStateLoss() + private fun showHiddenFragment(fragment: Fragment) { + val ft = supportFragmentManager.beginTransaction() + ft.setCustomAnimations(R.anim.fade_in_quick, R.anim.fade_out_quick) + val bottomBarFragments = getBottomBarFragments(fragment::class.java.name) + bottomBarFragments.forEach { + ft.hide(it) } + ft.show(fragment) + ft.commitAllowingStateLoss() } + private fun getBottomBarFragments(selectedFragmentName: String): List { + return getBottomNavFragmentNames() + .filter { it != selectedFragmentName } + .mapNotNull { supportFragmentManager.findFragmentByTag(it) } + } //endregion //region Back Stack override fun onBackPressed() { - if(isDrawerOpen) { + if (isDrawerOpen) { closeNavigationDrawer() return } @@ -825,19 +834,48 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. val topFragment = topFragment if (topFragment is ParentFragment) { if (!topFragment.handleBackPressed()) { - super.onBackPressed() + if (isBottomNavFragment(topFragment)) { + handleBottomNavBackStack() + } else { + super.onBackPressed() + } } } else { super.onBackPressed() } } + private fun handleBottomNavBackStack() { + if (bottomNavScreensStack.size == 0) { + finish() + } else if (bottomNavScreensStack.size == 1) { + bottomNavScreensStack.pop() + val previousFragment = supportFragmentManager.findFragmentByTag(navigationBehavior.homeFragmentClass.name) + if (previousFragment != null) { + showHiddenFragment(previousFragment) + applyCurrentFragmentTheme() + } + } else { + bottomNavScreensStack.pop() + val previousFragmentName = bottomNavScreensStack.peek() + val previousFragment = supportFragmentManager.findFragmentByTag(previousFragmentName) + if (previousFragment != null) { + showHiddenFragment(previousFragment) + applyCurrentFragmentTheme() + } + } + } + override val topFragment: Fragment? get() { val stackSize = supportFragmentManager.backStackEntryCount if (stackSize > 0) { - val fragmentTag = supportFragmentManager.getBackStackEntryAt(stackSize - 1).name - return supportFragmentManager.findFragmentByTag(fragmentTag) + val backStackEntryName = supportFragmentManager.getBackStackEntryAt(stackSize - 1).name + return if (backStackEntryName in getBottomNavFragmentNames()) { + currentFragment + } else { + supportFragmentManager.findFragmentByTag(backStackEntryName) + } } return null } @@ -852,7 +890,20 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. return null } - override val currentFragment: Fragment? get() = supportFragmentManager.findFragmentById(R.id.fullscreen) + override val currentFragment: Fragment? + get() { + val fragment = supportFragmentManager.findFragmentById(R.id.fullscreen) + return if (fragment != null && isBottomNavFragment(fragment)) { + val currentFragmentName = bottomNavScreensStack.peek() ?: navigationBehavior.homeFragmentClass.name + supportFragmentManager.findFragmentByTag(currentFragmentName) + } else { + fragment + } + } + + private fun isBottomNavFragment(fragment: Fragment?) = fragment?.arguments?.getBoolean(BOTTOM_NAV_SCREEN) == true + + private fun getBottomNavFragmentNames() = navigationBehavior.bottomNavBarFragments.map { it.name } private fun clearBackStack(cls: Class<*>?) { val fragment = topFragment @@ -942,16 +993,6 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. gauge.tag = gaugeLaunchDefinition } - override fun updateCalendarStartDay() { - //Restarts the CalendarListViewFragment to update the changed start day of the week - val fragment = supportFragmentManager.findFragmentByTag(CalendarFragment::class.java.name) as? ParentFragment - if (fragment != null) { - supportFragmentManager.beginTransaction().remove(fragment).commit() - } - val route = CalendarFragment.makeRoute() - addFragment(CalendarFragment.newInstance(route), route) - } - override fun addBookmark() { val dialog = BookmarkCreationDialog.newInstance(this, topFragment, peekingFragment) dialog?.show(supportFragmentManager, BookmarkCreationDialog::class.java.simpleName) @@ -1045,6 +1086,33 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. toast(R.string.errorOccurred) } + private fun createBottomNavFragment(name: String?): ParentFragment? { + return when (name) { + navigationBehavior.homeFragmentClass.name -> { + val route = navigationBehavior.createHomeFragmentRoute(ApiPrefs.user) + navigationBehavior.createHomeFragment(route) + } + CalendarFragment::class.java.name -> { + val route = CalendarFragment.makeRoute() + CalendarFragment.newInstance(route) + } + ToDoListFragment::class.java.name -> { + val route = ToDoListFragment.makeRoute(ApiPrefs.user!!) + ToDoListFragment.newInstance(route) + } + NotificationListFragment::class.java.name -> { + val route = NotificationListFragment.makeRoute(ApiPrefs.user!!) + NotificationListFragment.newInstance(route) + } + InboxFragment::class.java.name -> { + val route = InboxFragment.makeRoute() + InboxFragment.newInstance(route) + } + NothingToSeeHereFragment::class.java.name -> NothingToSeeHereFragment.newInstance() + else -> null + } + } + companion object { fun createIntent(context: Context, route: Route): Intent { return Intent(context, NavigationActivity::class.java).apply { putExtra(Route.ROUTE, route) } diff --git a/apps/student/src/main/java/com/instructure/student/activity/ShareFileUploadActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/ShareFileUploadActivity.kt index 5c6c6efc8e..10c7871031 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/ShareFileUploadActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/ShareFileUploadActivity.kt @@ -132,7 +132,6 @@ class ShareFileUploadActivity : AppCompatActivity(), ShareFileDestinationDialog. private fun getCourses() { loadCoursesJob = tryWeave { val courses = awaitApi> { CourseManager.getCourses(true, it) } - .filter { it.isNotDeleted() } if (courses.isNotEmpty()) { this@ShareFileUploadActivity.courses = ArrayList(courses) if (uploadFileSourceFragment == null) showDestinationDialog() diff --git a/apps/student/src/main/java/com/instructure/student/adapter/NotificationListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/NotificationListRecyclerAdapter.kt index 5543220bf3..a57e35d8c8 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/NotificationListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/NotificationListRecyclerAdapter.kt @@ -34,7 +34,6 @@ import com.instructure.canvasapi2.utils.ApiPrefs.user import com.instructure.canvasapi2.utils.ApiType import com.instructure.canvasapi2.utils.DateHelper import com.instructure.canvasapi2.utils.LinkHeaders -import com.instructure.canvasapi2.utils.isNotDeleted import com.instructure.pandarecycler.util.GroupSortedList.GroupComparatorCallback import com.instructure.pandarecycler.util.GroupSortedList.ItemComparatorCallback import com.instructure.pandarecycler.util.Types @@ -157,8 +156,7 @@ class NotificationListRecyclerAdapter( coursesCallback = object : StatusCallback>() { override fun onResponse(response: Response>, linkHeaders: LinkHeaders, type: ApiType) { - val courses = response.body()?.filter { it.isNotDeleted() } - courseMap = createCourseMap(courses) + courseMap = createCourseMap(response.body()) populateActivityStreamAdapter() } } diff --git a/apps/student/src/main/java/com/instructure/student/adapter/TodoListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/TodoListRecyclerAdapter.kt index 2c099903fb..f787c56283 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/TodoListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/TodoListRecyclerAdapter.kt @@ -225,7 +225,7 @@ open class TodoListRecyclerAdapter : ExpandableRecyclerAdapter>, linkHeaders: LinkHeaders, type: ApiType) { val body = response.body() ?: return val filteredCourses = body.filter { - !it.accessRestrictedByDate && !it.isInvited() && it.isNotDeleted() && (when (filterMode) { + !it.accessRestrictedByDate && !it.isInvited() && (when (filterMode) { is FavoritedCourses -> it.isFavorite else -> true }) diff --git a/apps/student/src/main/java/com/instructure/student/di/FragmentModule.kt b/apps/student/src/main/java/com/instructure/student/di/FragmentModule.kt new file mode 100644 index 0000000000..e5acd2200a --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/FragmentModule.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.di + +import androidx.fragment.app.FragmentActivity +import com.instructure.pandautils.navigation.WebViewRouter +import com.instructure.student.navigation.StudentWebViewRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +/** + * Module for various common Fragment scope dependencies that are used in different Fragments. + */ +@Module +@InstallIn(FragmentComponent::class) +class FragmentModule { + + @Provides + fun provideWebViewRouter(activity: FragmentActivity): WebViewRouter { + return StudentWebViewRouter(activity) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/elementary/GradesModule.kt b/apps/student/src/main/java/com/instructure/student/di/elementary/GradesModule.kt new file mode 100644 index 0000000000..466bcf9e9f --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/elementary/GradesModule.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.di.elementary + +import androidx.fragment.app.FragmentActivity +import com.instructure.pandautils.features.elementary.grades.GradesRouter +import com.instructure.student.mobius.elementary.grades.StudentGradesRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class GradesModule { + + @Provides + fun provideGradesRouter(activity: FragmentActivity): GradesRouter { + return StudentGradesRouter(activity) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/elementary/HomeroomModule.kt b/apps/student/src/main/java/com/instructure/student/di/elementary/HomeroomModule.kt index 9f97d2e0a0..a8147a721a 100644 --- a/apps/student/src/main/java/com/instructure/student/di/elementary/HomeroomModule.kt +++ b/apps/student/src/main/java/com/instructure/student/di/elementary/HomeroomModule.kt @@ -18,7 +18,7 @@ package com.instructure.student.di.elementary import androidx.fragment.app.FragmentActivity import com.instructure.pandautils.features.elementary.homeroom.HomeroomRouter -import com.instructure.student.mobius.elementary.StudentHomeroomRouter +import com.instructure.student.mobius.elementary.homeroom.StudentHomeroomRouter import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/apps/student/src/main/java/com/instructure/student/di/elementary/ResourcesModule.kt b/apps/student/src/main/java/com/instructure/student/di/elementary/ResourcesModule.kt new file mode 100644 index 0000000000..fc7e62e8a3 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/elementary/ResourcesModule.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.di.elementary + +import androidx.fragment.app.FragmentActivity +import com.instructure.pandautils.features.elementary.resources.itemviewmodels.ResourcesRouter +import com.instructure.student.mobius.elementary.resources.StudentResourcesRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class ResourcesModule { + + @Provides + fun provideResourcesRouter(activity: FragmentActivity): ResourcesRouter { + return StudentResourcesRouter(activity) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/elementary/schedule/ScheduleModule.kt b/apps/student/src/main/java/com/instructure/student/di/elementary/schedule/ScheduleModule.kt new file mode 100644 index 0000000000..2928c63b47 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/elementary/schedule/ScheduleModule.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.student.di.elementary.schedule + +import androidx.fragment.app.FragmentActivity +import com.instructure.pandautils.features.elementary.schedule.ScheduleRouter +import com.instructure.student.mobius.elementary.schedule.StudentScheduleRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class ScheduleModule { + + @Provides + fun provideScheduleRouter(activity: FragmentActivity): ScheduleRouter { + return StudentScheduleRouter(activity) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/dialog/CanvasContextListDialog.kt b/apps/student/src/main/java/com/instructure/student/dialog/CanvasContextListDialog.kt index e5f3d26215..4f059177aa 100644 --- a/apps/student/src/main/java/com/instructure/student/dialog/CanvasContextListDialog.kt +++ b/apps/student/src/main/java/com/instructure/student/dialog/CanvasContextListDialog.kt @@ -95,7 +95,7 @@ class CanvasContextListDialog : AppCompatDialogFragment() { { CourseManager.getCourses(forceNetwork, it) }, { GroupManager.getFavoriteGroups(it, forceNetwork) } ) - val validCourses = courses.filter { it.isFavorite && it.isValidTerm() && it.hasActiveEnrollment() && it.isNotDeleted() } + val validCourses = courses.filter { it.isFavorite && it.isValidTerm() && it.hasActiveEnrollment() } val courseMap = validCourses.associateBy { it.id } val validGroups = groups.filter { it.courseId == 0L || courseMap[it.courseId] != null } updateCanvasContexts(validCourses, validGroups) diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt index 936b4b7dc0..b23cadb342 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt @@ -83,12 +83,14 @@ class ApplicationSettingsFragment : ParentFragment() { legal.onClick { LegalDialogStyled().show(requireFragmentManager(), LegalDialogStyled.TAG) } pinAndFingerprint.setGone() // TODO: Wire up once implemented - pairObserver.setVisible() - pairObserver.onClick { - if (APIHelper.hasNetworkConnection()) { - addFragment(PairObserverFragment.newInstance()) - } else { - NoInternetConnectionDialog.show(requireFragmentManager()) + if (ApiPrefs.canGeneratePairingCode == true) { + pairObserver.setVisible() + pairObserver.onClick { + if (APIHelper.hasNetworkConnection()) { + addFragment(PairObserverFragment.newInstance()) + } else { + NoInternetConnectionDialog.show(requireFragmentManager()) + } } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/CalendarFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/CalendarFragment.kt index b0ec29293b..ee0ece3622 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/CalendarFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/CalendarFragment.kt @@ -23,6 +23,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import com.instructure.canvasapi2.models.PlannableType import com.instructure.canvasapi2.models.PlannerItem import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.pageview.PageView @@ -78,13 +79,13 @@ class CalendarFragment : ParentFragment() { private fun routeToItem(item: PlannerItem) { val route: Route? = when (item.plannableType) { - "assignment" -> { + PlannableType.ASSIGNMENT -> { AssignmentDetailsFragment.makeRoute(item.canvasContext, item.plannable.id) } - "discussion_topic" -> { + PlannableType.DISCUSSION_TOPIC -> { DiscussionDetailsFragment.makeRoute(item.canvasContext, item.plannable.id, title = item.plannable.title) } - "quiz" -> { + PlannableType.QUIZ -> { if (item.plannable.assignmentId != null) { // This is a quiz assignment, go to the assignment page AssignmentDetailsFragment.makeRoute(item.canvasContext, item.plannable.assignmentId!!) @@ -96,7 +97,7 @@ class CalendarFragment : ParentFragment() { } else null } } - "calendar_event" -> { + PlannableType.CALENDAR_EVENT -> { CalendarEventFragment.makeRoute(item.canvasContext, item.plannable.id) } else -> { diff --git a/apps/student/src/main/java/com/instructure/student/fragment/FlutterCalendarFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/FlutterCalendarFragment.kt index 3468d97569..f6953342f8 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/FlutterCalendarFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/FlutterCalendarFragment.kt @@ -109,7 +109,9 @@ class FlutterCalendarFragment : FlutterFragment() { // Perform onBackPressed on the FlutterFragment, which will attempt to pop the current route and update // the 'shouldPop' value for future use. - onBackPressed() + if (!shouldPop) { + onBackPressed() + } // If 'shouldPop' was true it means we just popped a CalendarScreen in Flutter and that we should also // allow this fragment to be popped diff --git a/apps/student/src/main/java/com/instructure/student/fragment/InboxComposeMessageFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/InboxComposeMessageFragment.kt index 6a4314cfad..515e5660a4 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/InboxComposeMessageFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/InboxComposeMessageFragment.kt @@ -60,6 +60,7 @@ class InboxComposeMessageFragment : ParentFragment() { private val includedMessageIds by LongArrayArg(key = Const.MESSAGE) private val isReply by BooleanArg(key = IS_REPLY) private val currentMessage by NullableParcelableArg(key = Const.MESSAGE_TO_USER) + private val homeroomMessage by BooleanArg(key = HOMEROOM_MESSAGE) private var selectedContext by NullableParcelableArg(key = Const.CANVAS_CONTEXT) private val isNewMessage by lazy { conversation == null } @@ -203,6 +204,16 @@ class InboxComposeMessageFragment : ParentFragment() { // Get courses and groups if this is a new compose message if (isNewMessage) getAllCoursesAndGroups() + + if (homeroomMessage) { + hideFieldsForHomeroomMessage() + } + } + + private fun hideFieldsForHomeroomMessage() { + spinnerWrapper.setGone() + recipientWrapper.setGone() + sendIndividualMessageWrapper.setGone() } private fun getAllCoursesAndGroups() { @@ -244,8 +255,10 @@ class InboxComposeMessageFragment : ParentFragment() { } private fun courseWasSelected() { - recipientWrapper.visibility = View.VISIBLE - contactsImageButton.visibility = View.VISIBLE + if (!homeroomMessage) { + recipientWrapper.visibility = View.VISIBLE + contactsImageButton.visibility = View.VISIBLE + } requireActivity().invalidateOptionsMenu() chips.canvasContext = selectedContext } @@ -442,6 +455,7 @@ class InboxComposeMessageFragment : ParentFragment() { private const val IS_REPLY = "is_reply" private const val PARTICIPANTS = "participants" + private const val HOMEROOM_MESSAGE = "homeroom_message" fun makeRoute( isReply: Boolean, @@ -464,11 +478,13 @@ class InboxComposeMessageFragment : ParentFragment() { fun makeRoute( canvasContext: CanvasContext, - participants: ArrayList + participants: ArrayList, + homeroomMessage: Boolean = false ): Route { val bundle = Bundle().apply { putParcelableArrayList(PARTICIPANTS, participants) putParcelable(Const.CANVAS_CONTEXT, canvasContext) + putBoolean(HOMEROOM_MESSAGE, homeroomMessage) } return Route(InboxComposeMessageFragment::class.java, canvasContext, bundle) } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/ElementaryDashboardFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/ElementaryDashboardFragment.kt index 2304999645..e7844fdb00 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/elementary/ElementaryDashboardFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/ElementaryDashboardFragment.kt @@ -20,14 +20,22 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.viewpager.widget.ViewPager import com.google.android.material.tabs.TabLayout import com.instructure.canvasapi2.models.CanvasContext import com.instructure.interactions.router.Route import com.instructure.pandautils.features.elementary.ElementaryDashboardPagerAdapter -import com.instructure.pandautils.utils.* +import com.instructure.pandautils.features.elementary.grades.GradesFragment +import com.instructure.pandautils.features.elementary.homeroom.HomeroomFragment +import com.instructure.pandautils.features.elementary.resources.ResourcesFragment +import com.instructure.pandautils.features.elementary.schedule.pager.SchedulePagerFragment +import com.instructure.pandautils.utils.Const +import com.instructure.pandautils.utils.ParcelableArg +import com.instructure.pandautils.utils.isTablet +import com.instructure.pandautils.utils.makeBundle import com.instructure.student.R +import com.instructure.student.databinding.FragmentElementaryDashboardBinding import com.instructure.student.fragment.ParentFragment -import com.instructure.student.util.FeatureFlagPrefs import kotlinx.android.synthetic.main.fragment_course_grid.toolbar import kotlinx.android.synthetic.main.fragment_elementary_dashboard.* @@ -35,6 +43,15 @@ class ElementaryDashboardFragment : ParentFragment() { private val canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) + private val schedulePagerFragment = SchedulePagerFragment.newInstance() + + private val fragments = listOf( + HomeroomFragment.newInstance(), + schedulePagerFragment, + GradesFragment.newInstance(), + ResourcesFragment.newInstance() + ) + override fun title(): String = if (isAdded) getString(R.string.dashboard) else "" override fun applyTheme() { @@ -42,19 +59,24 @@ class ElementaryDashboardFragment : ParentFragment() { navigation?.attachNavigationDrawer(this, toolbar) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = - layoutInflater.inflate(R.layout.fragment_elementary_dashboard, container, false) + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val binding = FragmentElementaryDashboardBinding.inflate(inflater, container, false) + binding.lifecycleOwner = this + binding.todayButtonVisibility = schedulePagerFragment.getTodayButtonVisibility() + + binding.todayButton.setOnClickListener { + schedulePagerFragment.jumpToToday() + } + + binding.dashboardPager.offscreenPageLimit = fragments.size + + return binding.root + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - if (!FeatureFlagPrefs.showInProgressK5Tabs) { - dashboardTabLayout.removeTabAt(3) - dashboardTabLayout.removeTabAt(2) - dashboardTabLayout.removeTabAt(1) - } - - dashboardPager.adapter = ElementaryDashboardPagerAdapter(canvasContext, childFragmentManager) + dashboardPager.adapter = ElementaryDashboardPagerAdapter(fragments, childFragmentManager) dashboardTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabReselected(tab: TabLayout.Tab?) = Unit @@ -63,9 +85,14 @@ class ElementaryDashboardFragment : ParentFragment() { override fun onTabSelected(tab: TabLayout.Tab?) { tab?.let { dashboardPager.setCurrentItem(it.position, !isTablet) + if (it.position != fragments.indexOf(schedulePagerFragment)) { + todayButton.visibility = View.GONE + } else { + todayButton.visibility = + if (schedulePagerFragment.getTodayButtonVisibility().value == true) View.VISIBLE else View.GONE + } } } - }) } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/grades/StudentGradesRouter.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/grades/StudentGradesRouter.kt new file mode 100644 index 0000000000..0475d8f222 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/grades/StudentGradesRouter.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.mobius.elementary.grades + +import androidx.fragment.app.FragmentActivity +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.features.elementary.grades.GradesRouter +import com.instructure.student.fragment.GradesListFragment +import com.instructure.student.router.RouteMatcher + +class StudentGradesRouter(private val activity: FragmentActivity) : GradesRouter { + + override fun openCourseGrades(course: Course) { + val route = GradesListFragment.makeRoute(course) + RouteMatcher.route(activity, route) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/StudentHomeroomRouter.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/homeroom/StudentHomeroomRouter.kt similarity index 71% rename from apps/student/src/main/java/com/instructure/student/mobius/elementary/StudentHomeroomRouter.kt rename to apps/student/src/main/java/com/instructure/student/mobius/elementary/homeroom/StudentHomeroomRouter.kt index 205df4e6f0..a4e73ebc8f 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/elementary/StudentHomeroomRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/homeroom/StudentHomeroomRouter.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.mobius.elementary +package com.instructure.student.mobius.elementary.homeroom import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.models.CanvasContext @@ -23,26 +23,12 @@ import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.features.elementary.homeroom.HomeroomRouter import com.instructure.student.flutterChannels.FlutterComm -import com.instructure.student.fragment.AnnouncementListFragment -import com.instructure.student.fragment.AssignmentListFragment -import com.instructure.student.fragment.CourseBrowserFragment -import com.instructure.student.fragment.DiscussionDetailsFragment +import com.instructure.student.fragment.* +import com.instructure.student.mobius.assignmentDetails.ui.AssignmentDetailsFragment import com.instructure.student.router.RouteMatcher class StudentHomeroomRouter(private val activity: FragmentActivity) : HomeroomRouter { - override fun canRouteInternally(url: String): Boolean { - return RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, routeIfPossible = false, allowUnsupported = false) - } - - override fun routeInternally(url: String) { - RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, routeIfPossible = true, allowUnsupported = false) - } - - override fun openMedia(url: String) { - RouteMatcher.openMedia(activity, url) - } - override fun openAnnouncements(canvasContext: CanvasContext) { val route = AnnouncementListFragment.makeRoute(canvasContext) RouteMatcher.route(activity, route) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/resources/StudentResourcesRouter.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/resources/StudentResourcesRouter.kt new file mode 100644 index 0000000000..c6c5ceff05 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/resources/StudentResourcesRouter.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.mobius.elementary.resources + +import androidx.fragment.app.FragmentActivity +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.features.elementary.resources.itemviewmodels.ResourcesRouter +import com.instructure.student.fragment.InboxComposeMessageFragment +import com.instructure.student.fragment.LtiLaunchFragment +import com.instructure.student.router.RouteMatcher + +class StudentResourcesRouter(private val activity: FragmentActivity) : ResourcesRouter { + + override fun openLti(ltiTool: LTITool) { + val course = Course(id = ltiTool.contextId ?: 0, name = ltiTool.contextName ?: "") + val route = LtiLaunchFragment.makeRoute( + course, + ltiTool.url ?: ltiTool.courseNavigation?.url ?: "", + ltiTool.courseNavigation?.text ?: ltiTool.name ?: "", + sessionLessLaunch = true, + isAssignmentLTI = false, + ltiTool = ltiTool) + RouteMatcher.route(activity, route) + } + + override fun openComposeMessage(user: User) { + val recipient = Recipient.from(user) + val context = Course(id = user.enrollments[0].courseId, homeroomCourse = true) + val route = InboxComposeMessageFragment.makeRoute(context, arrayListOf(recipient), homeroomMessage = true) + RouteMatcher.route(activity, route) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/schedule/StudentScheduleRouter.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/schedule/StudentScheduleRouter.kt new file mode 100644 index 0000000000..580c9ec153 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/schedule/StudentScheduleRouter.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.student.mobius.elementary.schedule + +import androidx.fragment.app.FragmentActivity +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.pandautils.features.elementary.schedule.ScheduleRouter +import com.instructure.student.fragment.BasicQuizViewFragment +import com.instructure.student.fragment.CalendarEventFragment +import com.instructure.student.fragment.CourseBrowserFragment +import com.instructure.student.fragment.DiscussionDetailsFragment +import com.instructure.student.mobius.assignmentDetails.ui.AssignmentDetailsFragment +import com.instructure.student.router.RouteMatcher + +class StudentScheduleRouter(private val activity: FragmentActivity) : ScheduleRouter { + + override fun openAssignment(canvasContext: CanvasContext, assignmentId: Long) { + RouteMatcher.route(activity, AssignmentDetailsFragment.makeRoute(canvasContext, assignmentId)) + } + + override fun openCalendarEvent(canvasContext: CanvasContext, scheduleItemId: Long) { + RouteMatcher.route(activity, CalendarEventFragment.makeRoute(canvasContext, scheduleItemId)) + } + + override fun openAnnouncementDetails(course: Course, announcement: DiscussionTopicHeader) { + RouteMatcher.route(activity, DiscussionDetailsFragment.makeRoute(course, announcement)) + } + + override fun openQuiz(canvasContext: CanvasContext, htmlUrl: String) { + RouteMatcher.route(activity, BasicQuizViewFragment.makeRoute(canvasContext, htmlUrl)) + } + + override fun openDiscussion(canvasContext: CanvasContext, discussionId: Long, discussionTitle: String) { + RouteMatcher.route( + activity, + DiscussionDetailsFragment.makeRoute(canvasContext, discussionId, title = discussionTitle) + ) + } + + override fun openCourse(course: Course) { + RouteMatcher.route(activity, CourseBrowserFragment.makeRoute(course)) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/navigation/DefaultNavigationBehavior.kt b/apps/student/src/main/java/com/instructure/student/navigation/DefaultNavigationBehavior.kt index 759c5d8e82..9229b7757e 100644 --- a/apps/student/src/main/java/com/instructure/student/navigation/DefaultNavigationBehavior.kt +++ b/apps/student/src/main/java/com/instructure/student/navigation/DefaultNavigationBehavior.kt @@ -19,6 +19,7 @@ package com.instructure.student.navigation import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.interactions.router.Route +import com.instructure.student.R import com.instructure.student.fragment.* class DefaultNavigationBehavior() : NavigationBehavior { @@ -42,6 +43,8 @@ class DefaultNavigationBehavior() : NavigationBehavior { override val shouldOverrideFont: Boolean get() = false + override val bottomBarMenu: Int = R.menu.bottom_bar_menu + override fun createHomeFragmentRoute(canvasContext: CanvasContext?): Route { return DashboardFragment.makeRoute(ApiPrefs.user) } diff --git a/apps/student/src/main/java/com/instructure/student/navigation/ElementaryNavigationBehavior.kt b/apps/student/src/main/java/com/instructure/student/navigation/ElementaryNavigationBehavior.kt index 3f2efbc00e..8e3291174c 100644 --- a/apps/student/src/main/java/com/instructure/student/navigation/ElementaryNavigationBehavior.kt +++ b/apps/student/src/main/java/com/instructure/student/navigation/ElementaryNavigationBehavior.kt @@ -19,6 +19,7 @@ package com.instructure.student.navigation import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.interactions.router.Route +import com.instructure.student.R import com.instructure.student.fragment.* import com.instructure.student.mobius.elementary.ElementaryDashboardFragment @@ -43,6 +44,8 @@ class ElementaryNavigationBehavior() : NavigationBehavior { override val shouldOverrideFont: Boolean get() = true + override val bottomBarMenu: Int = R.menu.bottom_bar_menu_elementary + override fun createHomeFragmentRoute(canvasContext: CanvasContext?): Route { return ElementaryDashboardFragment.makeRoute(ApiPrefs.user) } diff --git a/apps/student/src/main/java/com/instructure/student/navigation/NavigationBehavior.kt b/apps/student/src/main/java/com/instructure/student/navigation/NavigationBehavior.kt index d256245e6f..1c84ada09c 100644 --- a/apps/student/src/main/java/com/instructure/student/navigation/NavigationBehavior.kt +++ b/apps/student/src/main/java/com/instructure/student/navigation/NavigationBehavior.kt @@ -16,6 +16,7 @@ */ package com.instructure.student.navigation +import androidx.annotation.MenuRes import com.instructure.canvasapi2.models.CanvasContext import com.instructure.interactions.router.Route import com.instructure.student.fragment.ParentFragment @@ -35,6 +36,9 @@ interface NavigationBehavior { val shouldOverrideFont: Boolean + @get:MenuRes + val bottomBarMenu: Int + fun createHomeFragmentRoute(canvasContext: CanvasContext?): Route fun createHomeFragment(route: Route): ParentFragment diff --git a/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt b/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt new file mode 100644 index 0000000000..634e3baa55 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.navigation + +import androidx.fragment.app.FragmentActivity +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.navigation.WebViewRouter +import com.instructure.student.router.RouteMatcher + +class StudentWebViewRouter(val activity: FragmentActivity) : WebViewRouter { + + override fun canRouteInternally(url: String): Boolean { + return RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, routeIfPossible = false, allowUnsupported = false) + } + + override fun routeInternally(url: String) { + RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, routeIfPossible = true, allowUnsupported = false) + } + + override fun openMedia(url: String) { + RouteMatcher.openMedia(activity, url) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/util/FeatureFlagPrefs.kt b/apps/student/src/main/java/com/instructure/student/util/FeatureFlagPrefs.kt index cecc7965d6..251ba56c6a 100644 --- a/apps/student/src/main/java/com/instructure/student/util/FeatureFlagPrefs.kt +++ b/apps/student/src/main/java/com/instructure/student/util/FeatureFlagPrefs.kt @@ -15,10 +15,8 @@ */ package com.instructure.student.util -import com.instructure.canvasapi2.utils.FeatureFlagPref import com.instructure.canvasapi2.utils.PrefManager object FeatureFlagPrefs : PrefManager("feature_flags") { - var showInProgressK5Tabs by FeatureFlagPref("Show K5 in progress tabs") } diff --git a/apps/student/src/main/res/layout-sw720dp/fragment_elementary_dashboard.xml b/apps/student/src/main/res/layout-sw720dp/fragment_elementary_dashboard.xml index 26b1b51ce8..3c596cc38e 100644 --- a/apps/student/src/main/res/layout-sw720dp/fragment_elementary_dashboard.xml +++ b/apps/student/src/main/res/layout-sw720dp/fragment_elementary_dashboard.xml @@ -14,91 +14,108 @@ ~ along with this program. If not, see . ~ --> - + xmlns:tools="http://schemas.android.com/tools"> - + - + - + + - + - + android:layout_height="?android:attr/actionBarSize" + android:background="@color/defaultPrimary" + android:elevation="6dp" + app:layout_constraintTop_toTopOf="parent" + app:theme="@style/ToolBarStyle"> - + + + + android:background="@color/white" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar" + app:tabContentStart="8dp" + app:tabIconTint="@color/tab_layout_icon_tint" + app:tabIndicator="@drawable/tab_bar_indicator" + app:tabIndicatorColor="@color/blueAnnotation" + app:tabIndicatorFullWidth="false" + app:tabIndicatorHeight="3dp" + app:tabInlineLabel="true" + app:tabMode="scrollable" + app:tabPaddingEnd="12dp" + app:tabPaddingStart="8dp" + app:tabSelectedTextColor="@color/blueAnnotation" + app:tabTextAppearance="@android:style/TextAppearance.Widget.TabWidget" + app:tabTextColor="@color/defaultTextDark"> - + - - - - - + + + + + + + + + + + - \ No newline at end of file + + \ No newline at end of file diff --git a/apps/student/src/main/res/layout/activity_navigation.xml b/apps/student/src/main/res/layout/activity_navigation.xml index d244d0f333..6423d17983 100644 --- a/apps/student/src/main/res/layout/activity_navigation.xml +++ b/apps/student/src/main/res/layout/activity_navigation.xml @@ -58,8 +58,7 @@ android:layout_height="wrap_content" android:background="@color/white" app:elevation="0dp" - app:labelVisibilityMode="labeled" - app:menu="@menu/bottom_bar_menu" /> + app:labelVisibilityMode="labeled" /> diff --git a/apps/student/src/main/res/layout/fragment_application_settings.xml b/apps/student/src/main/res/layout/fragment_application_settings.xml index 4578a5b57d..9e3885f29a 100644 --- a/apps/student/src/main/res/layout/fragment_application_settings.xml +++ b/apps/student/src/main/res/layout/fragment_application_settings.xml @@ -74,7 +74,7 @@ android:layout_height="wrap_content" android:labelFor="@+id/toggle" android:textSize="16sp" - android:text="@string/settingsElementaryView" + android:text="@string/settingsHomeroomView" app:layout_constraintVertical_chainStyle="packed" app:layout_constraintBottom_toTopOf="@id/elementaryViewDescription" app:layout_constraintStart_toStartOf="parent" diff --git a/apps/student/src/main/res/layout/fragment_elementary_dashboard.xml b/apps/student/src/main/res/layout/fragment_elementary_dashboard.xml index 20fc5e183c..bd1330b2da 100644 --- a/apps/student/src/main/res/layout/fragment_elementary_dashboard.xml +++ b/apps/student/src/main/res/layout/fragment_elementary_dashboard.xml @@ -14,80 +14,106 @@ ~ along with this program. If not, see . ~ --> - + xmlns:tools="http://schemas.android.com/tools"> - + - + - + + - + - + android:layout_height="?android:attr/actionBarSize" + android:background="@color/defaultPrimary" + android:elevation="6dp" + app:layout_constraintTop_toTopOf="parent" + app:theme="@style/ToolBarStyle"> - + + + + android:background="@color/white" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar" + app:tabContentStart="16dp" + app:tabIconTint="@color/tab_layout_icon_tint" + app:tabIndicator="@drawable/tab_bar_indicator" + app:tabIndicatorColor="@color/blueAnnotation" + app:tabIndicatorFullWidth="false" + app:tabIndicatorHeight="3dp" + app:tabInlineLabel="true" + app:tabMode="scrollable" + app:tabPaddingEnd="12dp" + app:tabPaddingStart="8dp" + app:tabSelectedTextColor="@color/blueAnnotation" + app:tabTextAppearance="@android:style/TextAppearance.Widget.TabWidget" + app:tabTextColor="@color/defaultTextDark"> - + - + - + + + + + + + + + - \ No newline at end of file + + \ No newline at end of file diff --git a/apps/student/src/main/res/menu/bottom_bar_menu_elementary.xml b/apps/student/src/main/res/menu/bottom_bar_menu_elementary.xml new file mode 100644 index 0000000000..d4885b30ed --- /dev/null +++ b/apps/student/src/main/res/menu/bottom_bar_menu_elementary.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + diff --git a/apps/student/src/main/res/values/styles.xml b/apps/student/src/main/res/values/styles.xml index 8534714d6e..28460ec092 100644 --- a/apps/student/src/main/res/values/styles.xml +++ b/apps/student/src/main/res/values/styles.xml @@ -240,11 +240,6 @@ @color/textLightGray - - - - diff --git a/apps/teacher/build.gradle b/apps/teacher/build.gradle index 37c6e6fbd8..19face607e 100644 --- a/apps/teacher/build.gradle +++ b/apps/teacher/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.google.firebase.crashlytics' apply from: '../../gradle/coverage.gradle' apply plugin: 'dagger.hilt.android.plugin' -def updatePriority = 0 +def updatePriority = 2 def coverageEnabled = project.hasProperty('coverage') if (coverageEnabled) { @@ -43,8 +43,8 @@ android { defaultConfig { minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 42 - versionName = '1.14.0' + versionCode = 43 + versionName = '1.14.1' vectorDrawables.useSupportLibrary = true multiDexEnabled true testInstrumentationRunner 'com.instructure.teacher.ui.espresso.TeacherHiltTestRunner' diff --git a/apps/teacher/src/main/java/com/instructure/teacher/di/FragmentModule.kt b/apps/teacher/src/main/java/com/instructure/teacher/di/FragmentModule.kt new file mode 100644 index 0000000000..8c9b3a40f5 --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/di/FragmentModule.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.teacher.di + +import androidx.fragment.app.FragmentActivity +import com.instructure.pandautils.navigation.WebViewRouter +import com.instructure.teacher.navigation.TeacherWebViewRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +/** + * Module for various common Fragment scope dependencies that are used in different Fragments. + */ +@Module +@InstallIn(FragmentComponent::class) +class FragmentModule { + + @Provides + fun provideWebViewRouter(activity: FragmentActivity): WebViewRouter { + return TeacherWebViewRouter(activity) + } +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/di/elementary/GradesModule.kt b/apps/teacher/src/main/java/com/instructure/teacher/di/elementary/GradesModule.kt new file mode 100644 index 0000000000..300d1b9a0f --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/di/elementary/GradesModule.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.teacher.di.elementary + +import com.instructure.pandautils.features.elementary.grades.GradesRouter +import com.instructure.teacher.features.elementary.grades.TeacherGradesRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class GradesModule { + + @Provides + fun provideGradesRouter(): GradesRouter { + return TeacherGradesRouter() + } +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/di/elementary/HomeroomModule.kt b/apps/teacher/src/main/java/com/instructure/teacher/di/elementary/HomeroomModule.kt index 35bdddafc5..c77040569e 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/di/elementary/HomeroomModule.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/di/elementary/HomeroomModule.kt @@ -16,9 +16,8 @@ */ package com.instructure.teacher.di.elementary -import androidx.fragment.app.FragmentActivity import com.instructure.pandautils.features.elementary.homeroom.HomeroomRouter -import com.instructure.teacher.features.elementary.TeacherHomeroomRouter +import com.instructure.teacher.features.elementary.homeroom.TeacherHomeroomRouter import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/apps/teacher/src/main/java/com/instructure/teacher/di/elementary/ResourcesModule.kt b/apps/teacher/src/main/java/com/instructure/teacher/di/elementary/ResourcesModule.kt new file mode 100644 index 0000000000..183472a02b --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/di/elementary/ResourcesModule.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.teacher.di.elementary + +import com.instructure.pandautils.features.elementary.resources.itemviewmodels.ResourcesRouter +import com.instructure.teacher.features.elementary.resources.TeacherResourcesRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class ResourcesModule { + + @Provides + fun provideResourcesRouter(): ResourcesRouter { + return TeacherResourcesRouter() + } +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/di/elementary/schedule/ScheduleModule.kt b/apps/teacher/src/main/java/com/instructure/teacher/di/elementary/schedule/ScheduleModule.kt new file mode 100644 index 0000000000..65a11f63c9 --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/di/elementary/schedule/ScheduleModule.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.teacher.di.elementary.schedule + +import com.instructure.pandautils.features.elementary.schedule.ScheduleRouter +import com.instructure.teacher.features.elementary.TeacherScheduleRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class ScheduleModule { + + @Provides + fun provideScheduleRouter(): ScheduleRouter { + return TeacherScheduleRouter() + } +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/elementary/TeacherScheduleRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/elementary/TeacherScheduleRouter.kt new file mode 100644 index 0000000000..872fdff366 --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/elementary/TeacherScheduleRouter.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.teacher.features.elementary + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.pandautils.features.elementary.schedule.ScheduleRouter + +class TeacherScheduleRouter : ScheduleRouter { + override fun openAssignment(canvasContext: CanvasContext, assignmentId: Long) = Unit + + override fun openCalendarEvent(canvasContext: CanvasContext, scheduleItemId: Long) = Unit + + override fun openAnnouncementDetails(course: Course, announcement: DiscussionTopicHeader) = Unit + + override fun openQuiz(canvasContext: CanvasContext, htmlUrl: String) = Unit + + override fun openDiscussion(canvasContext: CanvasContext, discussionId: Long, discussionTitle: String) = Unit + + override fun openCourse(course: Course) = Unit +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/elementary/grades/TeacherGradesRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/elementary/grades/TeacherGradesRouter.kt new file mode 100644 index 0000000000..110c12d5de --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/elementary/grades/TeacherGradesRouter.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.teacher.features.elementary.grades + +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.features.elementary.grades.GradesRouter + +class TeacherGradesRouter : GradesRouter { + override fun openCourseGrades(course: Course) = Unit +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/elementary/TeacherHomeroomRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/elementary/homeroom/TeacherHomeroomRouter.kt similarity index 85% rename from apps/teacher/src/main/java/com/instructure/teacher/features/elementary/TeacherHomeroomRouter.kt rename to apps/teacher/src/main/java/com/instructure/teacher/features/elementary/homeroom/TeacherHomeroomRouter.kt index 24fcf77bba..0b065b75bb 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/elementary/TeacherHomeroomRouter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/elementary/homeroom/TeacherHomeroomRouter.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.features.elementary +package com.instructure.teacher.features.elementary.homeroom import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course @@ -22,11 +22,6 @@ import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.pandautils.features.elementary.homeroom.HomeroomRouter class TeacherHomeroomRouter : HomeroomRouter { - override fun canRouteInternally(url: String): Boolean = false - - override fun routeInternally(url: String) = Unit - - override fun openMedia(url: String) = Unit override fun openAnnouncements(canvasContext: CanvasContext) = Unit diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/elementary/resources/TeacherResourcesRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/elementary/resources/TeacherResourcesRouter.kt new file mode 100644 index 0000000000..281a7bf0aa --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/elementary/resources/TeacherResourcesRouter.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.teacher.features.elementary.resources + +import com.instructure.canvasapi2.models.LTITool +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.features.elementary.resources.itemviewmodels.ResourcesRouter + +class TeacherResourcesRouter : ResourcesRouter { + + override fun openLti(ltiTool: LTITool) = Unit + + override fun openComposeMessage(user: User) = Unit +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditAssignmentDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditAssignmentDetailsFragment.kt index 47cecb3246..451a908904 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditAssignmentDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditAssignmentDetailsFragment.kt @@ -461,7 +461,11 @@ class EditAssignmentDetailsFragment : BaseFragment() { } catch (e: Throwable) { saveButton?.setVisible() savingProgressBar.setGone() - toast(R.string.error_saving_assignment) + if (mAssignment.inClosedGradingPeriod) { + toast(R.string.error_saving_assignment_closed_grading_period) + } else { + toast(R.string.error_saving_assignment) + } } } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt new file mode 100644 index 0000000000..1632ce8fcb --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.teacher.navigation + +import androidx.fragment.app.FragmentActivity +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.navigation.WebViewRouter +import com.instructure.teacher.router.RouteMatcher + +class TeacherWebViewRouter(val activity: FragmentActivity) : WebViewRouter { + + override fun canRouteInternally(url: String): Boolean { + return RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, routeIfPossible = false) + } + + override fun routeInternally(url: String) { + RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, routeIfPossible = true) + } + + override fun openMedia(url: String) { + RouteMatcher.openMedia(activity, url) + } +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/view/grade_slider/SpeedGraderSlider.kt b/apps/teacher/src/main/java/com/instructure/teacher/view/grade_slider/SpeedGraderSlider.kt index 6e45e44609..2ae9f95e2c 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/view/grade_slider/SpeedGraderSlider.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/view/grade_slider/SpeedGraderSlider.kt @@ -36,7 +36,7 @@ import kotlin.math.roundToInt import kotlin.properties.Delegates class SpeedGraderSlider @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { var onGradeChanged: (String, Boolean) -> Unit by Delegates.notNull() @@ -44,6 +44,7 @@ class SpeedGraderSlider @JvmOverloads constructor( private lateinit var assignment: Assignment private var submission: Submission? = null private lateinit var assignee: Assignee + private var maxGradeValue: Int = 0 private var isExcused: Boolean = false private var notGraded: Boolean = false @@ -68,7 +69,25 @@ class SpeedGraderSlider @JvmOverloads constructor( accessibleTouchTarget() } - slider.max = 0 + minGrade.apply { + setOnClickListener { + updateGrade(0) + notGraded = false + isExcused = false + } + accessibleTouchTarget() + } + + maxGrade.apply { + setOnClickListener { + updateGrade(maxGradeValue) + notGraded = false + isExcused = false + } + accessibleTouchTarget() + } + + slider.max = maxGradeValue } fun setData(assignment: Assignment, submission: Submission?, assignee: Assignee) { @@ -111,17 +130,26 @@ class SpeedGraderSlider @JvmOverloads constructor( pointsPossibleView.setGone() } slider.progress = this.submission?.score?.div(this.assignment.pointsPossible)?.times(100)?.toInt() - ?: 0 + ?: 0 minGrade.text = "0%" } + maxGradeValue = slider.max + slider.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { if (!isExcused && !notGraded) { if (assignment.gradingType?.let { Assignment.getGradingTypeFromAPIString(it) } == Assignment.GradingType.PERCENT) { - EventBus.getDefault().post(ShowSliderGradeEvent(seekBar, this@SpeedGraderSlider.assignee.id, "$progress%")) + EventBus.getDefault() + .post(ShowSliderGradeEvent(seekBar, this@SpeedGraderSlider.assignee.id, "$progress%")) } else { - EventBus.getDefault().post(ShowSliderGradeEvent(seekBar, this@SpeedGraderSlider.assignee.id, progress.toString())) + EventBus.getDefault().post( + ShowSliderGradeEvent( + seekBar, + this@SpeedGraderSlider.assignee.id, + progress.toString() + ) + ) } } @@ -188,7 +216,8 @@ class SpeedGraderSlider @JvmOverloads constructor( val label: String if (assignment.gradingType?.let { Assignment.getGradingTypeFromAPIString(it) } == Assignment.GradingType.POINTS) { - anchorRect.left = anchorRect.left + slider.paddingLeft + (this.assignment.pointsPossible.toInt() * stepWidth).roundToInt() + anchorRect.left = + anchorRect.left + slider.paddingLeft + (this.assignment.pointsPossible.toInt() * stepWidth).roundToInt() label = NumberHelper.formatDecimal(this.assignment.pointsPossible, 0, true) } else { anchorRect.left = anchorRect.left + slider.paddingLeft + width / 2 diff --git a/apps/teacher/src/main/res/values-b+sv+instk12/strings.xml b/apps/teacher/src/main/res/values-b+sv+instk12/strings.xml index 2583a6ee5d..eb7a5025ae 100644 --- a/apps/teacher/src/main/res/values-b+sv+instk12/strings.xml +++ b/apps/teacher/src/main/res/values-b+sv+instk12/strings.xml @@ -355,7 +355,7 @@ Låt eleven slippa göra Låt gruppen slippa göra Fullständig - Ofullständig + Inte färdig Ej bedömd Ursäktad %s %s @@ -822,7 +822,7 @@ Dölj knappar för Skapa fil och Skapa mapp Avpublicera Begränsad åtkomst - Begränsad + Begränsat Dold, filerna inuti kommer att vara tillgängliga via länkar. Endast tillgänglig för elever med länk. Ej tillgänglig i elevfiler. Schemalägg elevens tillgänglighet diff --git a/apps/teacher/src/main/res/values-sv/strings.xml b/apps/teacher/src/main/res/values-sv/strings.xml index c5caf66da6..bd1a7dab53 100644 --- a/apps/teacher/src/main/res/values-sv/strings.xml +++ b/apps/teacher/src/main/res/values-sv/strings.xml @@ -354,7 +354,7 @@ Låt studenten slippa göra Låt gruppen slippa göra Färdig - ofullständig + Inte färdig Ej bedömd Ursäktad %s %s @@ -391,7 +391,7 @@ Inte inskickade, %s av %s Visa omdöme som… Procent - Färdig/ofullständig + Färdig/Inte färdig Poäng GPA-skala @@ -821,7 +821,7 @@ Dölj knappar för Skapa fil och Skapa mapp Avpublicera Begränsad åtkomst - Begränsad + Begränsat Dold, filerna inuti kommer att vara tillgängliga via länkar. Endast tillgänglig för studenter med länk. Ej tillgänglig i studentfiler. Schemalägg studentens tillgänglighet diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt index 960e0e0592..cad597cbe6 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt @@ -20,7 +20,6 @@ package com.instructure.canvas.espresso.mockCanvas import android.util.Log import com.github.javafaker.Faker -import com.instructure.canvas.espresso.mockCanvas.MockCanvas.Companion.data import com.instructure.canvas.espresso.mockCanvas.utils.Randomizer import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.models.canvadocs.CanvaDocAnnotation @@ -150,12 +149,15 @@ class MockCanvas { val assignmentGroups = mutableMapOf>() /** Map of assignment ID to assignment */ - val assignments = mutableMapOf() + val assignments = mutableMapOf() + + /** Map of todos */ + val todos = mutableListOf() /** Map of assignment ID to a list of submissions */ val submissions = mutableMapOf>() - var ltiTool: LTITool? = null + val ltiTools = mutableListOf() /** Map of course ids to discussion topic headers */ val courseDiscussionTopicHeaders = mutableMapOf>() @@ -448,15 +450,19 @@ fun MockCanvas.updateUserEnrollments() { } } -fun MockCanvas.addCourseWithEnrollment(user: User, enrollmentType: Enrollment.EnrollmentType) { - val course = addCourse() +fun MockCanvas.addCourseWithEnrollment(user: User, enrollmentType: Enrollment.EnrollmentType, score: Double = 0.0, grade: String = "", isHomeroom: Boolean = false): Course { + val course = addCourse(isHomeroom = isHomeroom) addEnrollment( user = user, course = course, type = enrollmentType, - courseSectionId = if(course.sections.count() > 0) course.sections.get(0).id else 0 + courseSectionId = if(course.sections.count() > 0) course.sections.get(0).id else 0, + currentScore = score, + currentGrade = grade ) + + return course } /** Creates a new Course and adds it to MockCanvas */ @@ -471,6 +477,15 @@ fun MockCanvas.addCourse( ): Course { val randomCourseName = Randomizer.randomCourseName() val endAt = if (concluded) OffsetDateTime.now().minusWeeks(1).toApiString() else null + + val gradingPeriodList = if (withGradingPeriod) { + val gradingPeriodId = this.newItemId() + val gradingPeriod = GradingPeriod(gradingPeriodId, "Grading Period $gradingPeriodId") + addGradingPeriod(id, gradingPeriod) + } else { + emptyList() + } + val course = Course( id = id, name = randomCourseName, @@ -481,7 +496,8 @@ fun MockCanvas.addCourse( isFavorite = isFavorite, sections = if (section != null) listOf(section) else listOf
(), isPublic = isPublic, - homeroomCourse = isHomeroom + homeroomCourse = isHomeroom, + gradingPeriods = gradingPeriodList ) courses += course.id to course @@ -490,22 +506,17 @@ fun MockCanvas.addCourse( val quizzesTab = Tab(position = 1, label = "Quizzes", visibility = "public", tabId = Tab.QUIZZES_ID) courseTabs += course.id to mutableListOf(assignmentsTab, quizzesTab) - if(withGradingPeriod) { - val id = this.newItemId() - val gradingPeriod = GradingPeriod(id, "Grading Period $id") - addGradingPeriod(course.id, gradingPeriod) - } - return course } -fun MockCanvas.addGradingPeriod(courseId : Long, gradingPeriod: GradingPeriod) { +fun MockCanvas.addGradingPeriod(courseId : Long, gradingPeriod: GradingPeriod): List { var gpList = courseGradingPeriods[courseId] if(gpList == null) { gpList = mutableListOf() courseGradingPeriods[courseId] = gpList } gpList.add(gradingPeriod) + return gpList } /** Adds the provided permissions to the course */ @@ -969,10 +980,10 @@ fun MockCanvas.addSubmissionForAssignment( return userRootSubmission } -fun MockCanvas.addLTITool(name: String, url: String): LTITool { - val ltiTool = LTITool(id = 123L, name = name, url = url) +fun MockCanvas.addLTITool(name: String, url: String, course: Course, id: Long = newItemId()): LTITool { + val ltiTool = LTITool(id = id, name = name, url = url, contextId = course.id, contextName = course.name) - this.ltiTool = ltiTool + this.ltiTools.add(ltiTool) return ltiTool } @@ -993,23 +1004,27 @@ fun MockCanvas.addTerm(name: String = Randomizer.randomEnrollmentTitle()): Term /** Creates a new Enrollment and adds it to MockCanvas */ fun MockCanvas.addEnrollment( - user: User, - course: Course, - type: Enrollment.EnrollmentType, - observedUser: User? = null, - courseSectionId: Long = 0 + user: User, + course: Course, + type: Enrollment.EnrollmentType, + observedUser: User? = null, + courseSectionId: Long = 0, + currentScore: Double = 88.1, + currentGrade: String = "B+" ): Enrollment { val enrollment = Enrollment( - id = enrollments.size + 1L, - role = type, - type = type, - courseId = course.id, - enrollmentState = "active", - userId = user.id, - observedUser = observedUser, - grades = Grades(currentScore = 88.1, currentGrade = "B+"), - courseSectionId = courseSectionId, - user = user + id = enrollments.size + 1L, + role = type, + type = type, + courseId = course.id, + enrollmentState = "active", + userId = user.id, + observedUser = observedUser, + grades = Grades(currentScore = currentScore, currentGrade = currentGrade), + courseSectionId = courseSectionId, + user = user, + computedCurrentScore = currentScore, + computedCurrentGrade = currentGrade ) enrollments += enrollment.id to enrollment course.enrollments?.add(enrollment) // You won't see grades in the dashboard unless the course has enrollments @@ -1931,7 +1946,7 @@ fun MockCanvas.addSubmissionStreamItem( // Record the StreamItem var list = streamItems[user.id] - if(list == null) { + if (list == null) { list = mutableListOf() streamItems[user.id] = list } @@ -1939,4 +1954,23 @@ fun MockCanvas.addSubmissionStreamItem( // Return the StreamItem return item +} + +fun MockCanvas.addTodo(name: String, userId: Long, courseId: Long? = null, date: Date? = null): PlannerItem { + val todo = PlannerItem( + courseId, + null, + userId, + null, + null, + PlannableType.TODO, + Plannable(newItemId(), name, courseId, null, userId, null, date, null, date.toApiString()), + date ?: Date(), + null, + null, + null + ) + + todos.add(todo) + return todo } \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt index 508fe95d8c..0d0141b762 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt @@ -20,10 +20,7 @@ import android.util.Log import com.instructure.canvas.espresso.mockCanvas.Endpoint import com.instructure.canvas.espresso.mockCanvas.endpoint import com.instructure.canvas.espresso.mockCanvas.utils.* -import com.instructure.canvasapi2.models.QuizSubmissionQuestion -import com.instructure.canvasapi2.models.QuizSubmissionQuestionResponse -import com.instructure.canvasapi2.models.ScheduleItem -import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.models.* import okio.Buffer /** @@ -122,6 +119,27 @@ object ApiEndpoint : Endpoint( request.successResponse(result) } } + ), + Segment("external_tools") to Endpoint( + Segment("visible_course_nav_tools") to Endpoint { + GET { + val contextCodes = request.url().queryParameterValues("context_codes[]") + val courseIds = contextCodes.map { it.substringAfter("_").toLong() } + + val ltiToolsForCourse = data.ltiTools.filter { + courseIds.contains(it.contextId ?: 0) + } + request.successResponse(ltiToolsForCourse) + } + } + ), + Segment("planner") to Endpoint( + Segment("overrides") to Endpoint { + POST { + val plannerOverride = getJsonFromRequestBody(request.body()) + request.successResponse(plannerOverride!!) + } + } ) ) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ExternalToolsEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ExternalToolsEndpoints.kt index c511f03b73..7529f99f3e 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ExternalToolsEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ExternalToolsEndpoints.kt @@ -23,8 +23,10 @@ import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse object ExternalToolsEndpoint : Endpoint( response = { GET { // Only currently used for AssignmentDetailsInteractionTest.testQuizzesNext, which is commented out - if(data.ltiTool != null) { - request.successResponse(data.ltiTool!!) + val course = data.courses[pathVars.courseId]!! + val result = data.ltiTools.filter { it.contextId == course.id } + if(!result.isNullOrEmpty()) { + request.successResponse(result) } else { request.unauthorizedResponse() } diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/UserEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/UserEndpoints.kt index a4c1f243f1..d69c60f645 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/UserEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/UserEndpoints.kt @@ -19,14 +19,7 @@ package com.instructure.canvas.espresso.mockCanvas.endpoints import com.instructure.canvas.espresso.mockCanvas.Endpoint import com.instructure.canvas.espresso.mockCanvas.addFileToFolder import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.UserId -import com.instructure.canvas.espresso.mockCanvas.utils.successPaginatedResponse -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse -import com.instructure.canvas.espresso.mockCanvas.utils.user +import com.instructure.canvas.espresso.mockCanvas.utils.* import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.pageview.PandataInfo import okio.Buffer @@ -73,14 +66,19 @@ object UserEndpoint : Endpoint( val userId = pathVars.userId val userCourseIds = data.enrollments.values.filter {it.userId == userId}.map {it -> it.courseId} + val todos = data.todos.filter { it.userId == userId } + // Gather our assignments // Currently we assume all the assignments are due today val plannerItemsList = data.assignments.values .filter { userCourseIds.contains(it.courseId) } .map { - val plannable = Plannable(it.id, it.name ?: "", it.courseId, null, userId, null, null, it.id) - PlannerItem(it.courseId, null, userId, null, null, "assignment", plannable, Date(), null, SubmissionState()) + val plannableDate = it.dueDate ?: Date() + val plannable = Plannable(it.id, it.name + ?: "", it.courseId, null, userId, null, it.dueDate, it.id, null) + PlannerItem(it.courseId, null, userId, null, null, PlannableType.ASSIGNMENT, plannable, plannableDate, null, SubmissionState(), false) } + .plus(todos) request.successResponse(plannerItemsList) } diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/NestedScrollViewExtension.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/NestedScrollViewExtension.kt index fdd45069e8..89a162df2d 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/NestedScrollViewExtension.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/NestedScrollViewExtension.kt @@ -21,6 +21,7 @@ import android.widget.HorizontalScrollView import android.widget.ListView import android.widget.ScrollView import androidx.core.widget.NestedScrollView +import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.ViewAction import androidx.test.espresso.action.ViewActions import androidx.test.espresso.matcher.ViewMatchers @@ -33,6 +34,7 @@ class NestedScrollViewExtension(scrolltoAction: ViewAction = ViewActions.scrollT ViewMatchers.isDescendantOfA(Matchers.anyOf(ViewMatchers.isAssignableFrom(NestedScrollView::class.java), ViewMatchers.isAssignableFrom(ScrollView::class.java), ViewMatchers.isAssignableFrom(HorizontalScrollView::class.java), + ViewMatchers.isAssignableFrom(RecyclerView::class.java), ViewMatchers.isAssignableFrom(ListView::class.java)))) } } \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/SubmissionStateTypeAdapter.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/SubmissionStateTypeAdapter.kt index f16ffef97f..aebfa80881 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/SubmissionStateTypeAdapter.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/SubmissionStateTypeAdapter.kt @@ -28,7 +28,13 @@ class SubmissionStateTypeAdapter : JsonDeserializer { if (json?.isJsonObject == true) { val submitted = json.asJsonObject?.get("submitted")?.asBoolean ?: false val missing = json.asJsonObject?.get("missing")?.asBoolean ?: false - return SubmissionState(submitted, missing) + val late = json.asJsonObject?.get("late")?.asBoolean ?: false + val excused = json.asJsonObject?.get("excused")?.asBoolean ?: false + val graded = json.asJsonObject?.get("graded")?.asBoolean ?: false + val needsGrading = json.asJsonObject?.get("needs_grading")?.asBoolean ?: false + val withFeedback = json.asJsonObject?.get("with_feedback")?.asBoolean ?: false + val redoRequest = json.asJsonObject?.get("redo_request")?.asBoolean ?: false + return SubmissionState(submitted, missing, late, excused, graded, needsGrading, withFeedback, redoRequest) } else { return null } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt index a245a692a1..19341539c4 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt @@ -38,7 +38,7 @@ object CourseAPI { @get:GET("dashboard/dashboard_cards") val dashboardCourses: Call> - @get:GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=current_and_concluded") + @get:GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=completed&state[]=available") val firstPageCourses: Call> @get:GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=current_and_concluded") @@ -47,6 +47,9 @@ object CourseAPI { @get:GET("courses?include[]=term&include[]=syllabus_body&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=completed&state[]=available&include[]=observed_users") val firstPageCoursesWithSyllabus: Call> + @get:GET("courses?include[]=term&include[]=syllabus_body&include[]=license&include[]=is_public&include[]=permissions&enrollment_state=active") + val firstPageCoursesWithSyllabusWithActiveEnrollment: Call> + @get:GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=completed&state[]=available&state[]=unpublished") val firstPageCoursesTeacher: Call> @@ -104,6 +107,9 @@ object CourseAPI { @GET("courses/{courseId}/rubrics/{rubricId}") fun getRubricSettings(@Path("courseId") courseId: Long, @Path("rubricId") rubricId: Long): Call + + @GET("courses?include[]=total_scores&include[]=current_grading_period_scores&include[]=grading_periods&include[]=course_image&enrollment_state=active") + fun getFirstPageCoursesWithGrades(): Call> } @Throws(IOException::class) @@ -158,6 +164,10 @@ object CourseAPI { callback.addCall(adapter.build(CoursesInterface::class.java, params).firstPageCoursesWithSyllabus).enqueue(callback) } + fun getFirstPageCoursesWithSyllabusWithActiveEnrollment(adapter: RestBuilder, callback: StatusCallback>, params: RestParams) { + callback.addCall(adapter.build(CoursesInterface::class.java, params).firstPageCoursesWithSyllabusWithActiveEnrollment).enqueue(callback) + } + fun getFirstPageCoursesTeacher(adapter: RestBuilder, callback: StatusCallback>, params: RestParams) { callback.addCall(adapter.build(CoursesInterface::class.java, params).firstPageCoursesTeacher).enqueue(callback) } @@ -241,4 +251,8 @@ object CourseAPI { fun getRubricSettings(courseId: Long, rubricId: Long, adapter: RestBuilder, callback: StatusCallback, params: RestParams) { callback.addCall(adapter.build(CoursesInterface::class.java, params).getRubricSettings(courseId, rubricId)).enqueue(callback) } + + fun getFirstPageCoursesWithGrades(adapter: RestBuilder, callback: StatusCallback>, params: RestParams) { + callback.addCall(adapter.build(CoursesInterface::class.java, params).getFirstPageCoursesWithGrades()).enqueue(callback) + } } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/EnrollmentAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/EnrollmentAPI.kt index 2b36f951b2..dd684ab2c1 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/EnrollmentAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/EnrollmentAPI.kt @@ -62,6 +62,9 @@ object EnrollmentAPI { @POST("courses/{courseId}/enrollments/{enrollmentId}/{action}") fun handleInvite(@Path("courseId") courseId: Long, @Path("enrollmentId") enrollmentId: Long, @Path("action") action: String): Call + + @GET("users/self/enrollments?state[]=active&type[]=StudentEnrollment") + fun getFirstPageEnrollmentsForGradingPeriod(@Query("grading_period_id") gradingPeriodId: Long): Call> } fun getFirstPageEnrollmentsForCourse( @@ -117,4 +120,16 @@ object EnrollmentAPI { fun handleInvite(courseId: Long, enrollmentId: Long, acceptInvite: Boolean, adapter: RestBuilder, params: RestParams, callback: StatusCallback) { callback.addCall(adapter.build(EnrollmentInterface::class.java, params).handleInvite(courseId, enrollmentId, if (acceptInvite) "accept" else "reject")).enqueue(callback) } + + fun getEnrollmentsForGradingPeriod( + gradingPeriodId: Long, + adapter: RestBuilder, + params: RestParams, + callback: StatusCallback>) { + if (StatusCallback.isFirstPage(callback.linkHeaders)) { + callback.addCall(adapter.build(EnrollmentInterface::class.java, params).getFirstPageEnrollmentsForGradingPeriod(gradingPeriodId)).enqueue(callback) + } else if (callback.linkHeaders != null && StatusCallback.moreCallsExist(callback.linkHeaders)) { + callback.addCall(adapter.build(EnrollmentInterface::class.java, params).getNextPage(callback.linkHeaders!!.nextUrl!!)).enqueue(callback) + } + } } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ExternalToolAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ExternalToolAPI.kt index 6a007c9a44..31ef290017 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ExternalToolAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ExternalToolAPI.kt @@ -25,6 +25,7 @@ import com.instructure.canvasapi2.models.LTITool import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Path +import retrofit2.http.Query import retrofit2.http.Url @@ -35,6 +36,9 @@ internal object ExternalToolAPI { @GET("{contextId}/external_tools?include_parents=true") fun getExternalToolsForCanvasContext(@Path("contextId") contextId: Long): Call> + @GET("external_tools/visible_course_nav_tools") + fun getExternalToolsForCourses(@Query("context_codes[]", encoded = true) contextCodes: List): Call> + @GET fun getLtiFromUrl(@Url url: String): Call } @@ -47,6 +51,14 @@ internal object ExternalToolAPI { callback.addCall(adapter.build(ExternalToolInterface::class.java, params).getExternalToolsForCanvasContext(canvasContextId)).enqueue(callback) } + fun getExternalToolsForCourses( + canvasContextIds: List, + adapter: RestBuilder, + params: RestParams, + callback: StatusCallback>) { + callback.addCall(adapter.build(ExternalToolInterface::class.java, params).getExternalToolsForCourses(canvasContextIds)).enqueue(callback) + } + fun getLtiFromUrlSynchronous(url: String, adapter: RestBuilder, params: RestParams): LTITool? { try { val response = adapter.build(ExternalToolInterface::class.java, params).getLtiFromUrl(url).execute() diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/PlannerAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/PlannerAPI.kt index 4379a65ab1..0e7252cda0 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/PlannerAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/PlannerAPI.kt @@ -6,10 +6,10 @@ import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.PlannerItem +import com.instructure.canvasapi2.models.PlannerOverride +import com.instructure.canvasapi2.utils.weave.apiAsync import retrofit2.Call -import retrofit2.http.GET -import retrofit2.http.Path -import retrofit2.http.Query +import retrofit2.http.* object PlannerAPI { @@ -18,9 +18,24 @@ object PlannerAPI { @GET("users/self/planner/items") fun getPlannerItems(@Query("start_date") startDate: String?, @Query("end_date") endDate: String?): Call> + + @POST("planner/overrides") + fun createPlannerOverride(@Body plannerOverride: PlannerOverride): Call + + @FormUrlEncoded + @PUT("planner/overrides/{overrideId}") + fun updatePlannerOverride(@Path("overrideId") plannerOverrideId: Long, @Field("marked_complete") complete: Boolean): Call } fun getPlannerItems(adapter: RestBuilder, callback: StatusCallback>, params: RestParams, startDate: String? = null, endDate: String? = null) { callback.addCall(adapter.build(PlannerInterface::class.java, params).getPlannerItems(startDate, endDate)).enqueue(callback) } + + fun createPlannerOverride(adapter: RestBuilder, callback: StatusCallback, params: RestParams, plannerOverride: PlannerOverride) { + callback.addCall(adapter.build(PlannerInterface::class.java, params).createPlannerOverride(plannerOverride)).enqueue(callback) + } + + fun updatePlannerOverride(adapter: RestBuilder, callback: StatusCallback, params: RestParams, id: Long, complete: Boolean) { + callback.addCall(adapter.build(PlannerInterface::class.java, params).updatePlannerOverride(id, complete)).enqueue(callback) + } } \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ToDoAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ToDoAPI.kt index 5984d94231..d5e7fa7bdb 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ToDoAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ToDoAPI.kt @@ -70,4 +70,8 @@ object ToDoAPI { } } + fun getCourseTodos(canvasContext: CanvasContext, adapter: RestBuilder, params: RestParams, callback: StatusCallback>) { + callback.addCall(adapter.build(ToDosInterface::class.java, params).getCourseTodos(canvasContext.id)).enqueue(callback) + } + } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UserAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UserAPI.kt index 8a44e9f74e..1b3e14e107 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UserAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UserAPI.kt @@ -101,6 +101,9 @@ object UserAPI { @GET fun getNextPageMissingSubmissions(@Url nextUrl: String): Call> + + @GET("courses/{courseId}/users?enrollment_type[]=teacher&enrollment_type[]=ta&include[]=avatar_url&include[]=bio&include[]=enrollments") + fun getFirstPageTeacherListForCourse(@Path("courseId") courseId: Long): Call> } fun getColors(adapter: RestBuilder, callback: StatusCallback, params: RestParams) { @@ -244,4 +247,12 @@ object UserAPI { val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) callback.addCall(adapter.build(UsersInterface::class.java, params).getNextPageMissingSubmissions(nextUrl)).enqueue(callback) } + + fun getTeacherListForCourse(adapter: RestBuilder, params: RestParams, courseId: Long, callback: StatusCallback>) { + if (StatusCallback.isFirstPage(callback.linkHeaders)) { + callback.addCall(adapter.build(UsersInterface::class.java, params).getFirstPageTeacherListForCourse(courseId)).enqueue(callback) + } else if (callback.linkHeaders != null && StatusCallback.moreCallsExist(callback.linkHeaders)) { + callback.addCall(adapter.build(UsersInterface::class.java, params).next(callback.linkHeaders!!.nextUrl!!)).enqueue(callback) + } + } } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt index 1882630543..e7746b1e7b 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt @@ -1,7 +1,9 @@ package com.instructure.canvasapi2.di +import com.instructure.canvasapi2.apis.CalendarEventAPI import com.instructure.canvasapi2.apis.HelpLinksAPI import com.instructure.canvasapi2.apis.PlannerAPI +import com.instructure.canvasapi2.apis.ToDoAPI import com.instructure.canvasapi2.managers.* import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.RemoteConfigUtils @@ -60,6 +62,21 @@ object ApiModule { return UserManager } + @Provides + fun provideToDoManager(): ToDoManager { + return ToDoManager + } + + @Provides + fun provideEnrollmentManager(): EnrollmentManager { + return EnrollmentManager + } + + @Provides + fun provideExternalToolManager(): ExternalToolManager { + return ExternalToolManager + } + @Provides @Singleton fun provideHelpLinksApi(): HelpLinksAPI { @@ -77,4 +94,14 @@ object ApiModule { fun providePlannerApi(): PlannerAPI { return PlannerAPI } + + @Provides + fun provideAssignmentManager(): AssignmentManager { + return AssignmentManager + } + + @Provides + fun provideCalendarEventManager(): CalendarEventManager { + return CalendarEventManager + } } \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/CalendarEventManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/CalendarEventManager.kt index ea4ecd63fb..079bde49db 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/CalendarEventManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/CalendarEventManager.kt @@ -92,6 +92,8 @@ object CalendarEventManager { CalendarEventAPI.getCalendarEvent(eventId, adapter, params, callback) } + fun getCalendarEventAsync(eventId: Long, forceNetwork: Boolean) = apiAsync { getCalendarEvent(eventId, it, forceNetwork) } + fun deleteCalendarEvent(eventId: Long, cancelReason: String, callback: StatusCallback) { val adapter = RestBuilder(callback) val params = RestParams() diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/CourseManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/CourseManager.kt index 29f32aef11..da232075cb 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/CourseManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/CourseManager.kt @@ -26,7 +26,6 @@ import com.instructure.canvasapi2.models.postmodels.UpdateCourseWrapper import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.ExhaustiveListCallback -import com.instructure.canvasapi2.utils.isNotDeleted import com.instructure.canvasapi2.utils.weave.apiAsync import kotlinx.coroutines.Deferred import java.io.IOException @@ -116,6 +115,22 @@ object CourseManager { CourseAPI.getFirstPageCoursesWithSyllabus(adapter, depaginatedCallback, params) } + fun getCoursesWithSyllabusAsyncWithActiveEnrollmentAsync(forceNetwork: Boolean) = apiAsync> { getCoursesWithSyllabusWithActiveEnrollment(forceNetwork, it) } + + private fun getCoursesWithSyllabusWithActiveEnrollment(forceNetwork: Boolean, callback: StatusCallback>) { + val adapter = RestBuilder(callback) + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + + val depaginatedCallback = object : ExhaustiveListCallback(callback) { + override fun getNextPage(callback: StatusCallback>, nextUrl: String, isCached: Boolean) { + CourseAPI.getNextPageCourses(forceNetwork, nextUrl, adapter, callback) + } + } + + adapter.statusCallback = depaginatedCallback + CourseAPI.getFirstPageCoursesWithSyllabusWithActiveEnrollment(adapter, depaginatedCallback, params) + } + fun getCoursesTeacher(forceNetwork: Boolean, callback: StatusCallback>) { val adapter = RestBuilder(callback) val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) @@ -312,7 +327,7 @@ object CourseManager { val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) val data = CourseAPI.getCoursesSynchronously(adapter, params) - return data?.filter { it.isNotDeleted() } ?: ArrayList() + return data ?: ArrayList() } fun createCourseMap(courses: List?): Map = courses?.associateBy { it.id } ?: emptyMap() @@ -323,4 +338,20 @@ object CourseManager { CourseAPI.getRubricSettings(courseId, rubricId, adapter, callback, params) } + fun getCoursesWithGradesAsync(forceNetwork: Boolean) = apiAsync> { getCoursesWithGrades(forceNetwork, it) } + + private fun getCoursesWithGrades(forceNetwork: Boolean, callback: StatusCallback>) { + val adapter = RestBuilder(callback) + val params = RestParams(isForceReadFromNetwork = forceNetwork) + + val depaginatedCallback = object : ExhaustiveListCallback(callback) { + override fun getNextPage(callback: StatusCallback>, nextUrl: String, isCached: Boolean) { + CourseAPI.getNextPageCourses(forceNetwork, nextUrl, adapter, callback) + } + } + + adapter.statusCallback = depaginatedCallback + CourseAPI.getFirstPageCoursesWithGrades(adapter, callback, params) + } + } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/EnrollmentManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/EnrollmentManager.kt index 08de07c4e4..b03be81342 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/EnrollmentManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/EnrollmentManager.kt @@ -100,4 +100,25 @@ object EnrollmentManager { EnrollmentAPI.handleInvite(courseId, enrollmentId, acceptInvite, adapter, params, callback) } + fun getEnrollmentsForGradingPeriodAsync( + gradingPeriodId: Long, + forceNetwork: Boolean + ) = apiAsync> { getEnrollmentsForGradingPeriod(gradingPeriodId, forceNetwork, it) } + + private fun getEnrollmentsForGradingPeriod( + gradingPeriodId: Long, + forceNetwork: Boolean, + callback: StatusCallback> + ) { + val adapter = RestBuilder(callback) + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + val depaginatedCallback = object : ExhaustiveListCallback(callback) { + override fun getNextPage(callback: StatusCallback>, nextUrl: String, isCached: Boolean) { + EnrollmentAPI.getEnrollmentsForGradingPeriod(gradingPeriodId, adapter, params, callback) + } + } + adapter.statusCallback = depaginatedCallback + EnrollmentAPI.getEnrollmentsForGradingPeriod(gradingPeriodId, adapter, params, depaginatedCallback) + } + } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/ExternalToolManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/ExternalToolManager.kt index 2700df49a3..b76f090852 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/ExternalToolManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/ExternalToolManager.kt @@ -45,4 +45,20 @@ object ExternalToolManager { forceNetwork: Boolean ) = apiAsync> { getExternalToolsForCanvasContext(canvasContext, it, forceNetwork) } + fun getExternalToolsForCoursesAsync(ids: List, forceNetwork: Boolean) = apiAsync> { getExternalToolsForCourses(ids, it, forceNetwork) } + + private fun getExternalToolsForCourses( + ids: List, + callback: StatusCallback>, + forceNetwork: Boolean + ) { + val adapter = RestBuilder(callback) + val params = RestParams( + isForceReadFromNetwork = forceNetwork, + usePerPageQueryParam = true + ) + + ExternalToolAPI.getExternalToolsForCourses(ids, adapter, params, callback) + } + } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/PlannerManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/PlannerManager.kt index b1699d15d8..8cd21e8663 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/PlannerManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/PlannerManager.kt @@ -24,6 +24,7 @@ import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.PlannerItem +import com.instructure.canvasapi2.models.PlannerOverride import com.instructure.canvasapi2.utils.weave.apiAsync class PlannerManager(private val plannerApi: PlannerAPI) { @@ -31,14 +32,41 @@ class PlannerManager(private val plannerApi: PlannerAPI) { fun getPlannerItemsAsync(forceNetwork: Boolean, startDate: String? = null, endDate: String? = null) = apiAsync> { getPlannerItems(forceNetwork, it, startDate, endDate) } private fun getPlannerItems( - forceNetwork: Boolean, - callback: StatusCallback>, - startDate: String? = null, - endDate: String? = null + forceNetwork: Boolean, + callback: StatusCallback>, + startDate: String? = null, + endDate: String? = null ) { val adapter = RestBuilder(callback) val params = RestParams(isForceReadFromNetwork = forceNetwork) plannerApi.getPlannerItems(adapter, callback, params, startDate, endDate) } + + private fun createPlannerOverride( + forceNetwork: Boolean, + callback: StatusCallback, + plannerOverride: PlannerOverride + ) { + val adapter = RestBuilder(callback) + val params = RestParams(isForceReadFromNetwork = forceNetwork) + + plannerApi.createPlannerOverride(adapter, callback, params, plannerOverride) + } + + fun createPlannerOverrideAsync(forceNetwork: Boolean, plannerOverride: PlannerOverride) = apiAsync { createPlannerOverride(forceNetwork, it, plannerOverride) } + + private fun updatePlannerOverride( + forceNetwork: Boolean, + callback: StatusCallback, + completed: Boolean, + overrideId: Long + ) { + val adapter = RestBuilder(callback) + val params = RestParams(isForceReadFromNetwork = forceNetwork) + + plannerApi.updatePlannerOverride(adapter, callback, params, overrideId, completed) + } + + fun updatePlannerOverrideAsync(forceNetwork: Boolean, completed: Boolean, overrideId: Long) = apiAsync { updatePlannerOverride(forceNetwork, it, completed, overrideId) } } \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/ToDoManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/ToDoManager.kt index 7264ee0c36..22821f40de 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/ToDoManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/ToDoManager.kt @@ -22,6 +22,7 @@ import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.ToDo +import com.instructure.canvasapi2.utils.weave.apiAsync import java.util.* import kotlin.collections.HashSet @@ -33,6 +34,8 @@ object ToDoManager { ToDoAPI.getUserTodos(adapter, params, callback) } + fun getUserTodosAsync(forceNetwork: Boolean) = apiAsync> { getUserTodos(it, forceNetwork) } + fun getUserTodosWithUngradedQuizzes(callback: StatusCallback>, forceNetwork: Boolean) { val adapter = RestBuilder(callback) val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) @@ -69,20 +72,28 @@ object ToDoManager { return ToDoAPI.getCourseTodosSynchronous(canvasContext, adapter, params) } + fun getCourseTodos(canvasContext: CanvasContext, forceNetwork: Boolean, callback: StatusCallback>) { + val adapter = RestBuilder() + val params = RestParams(isForceReadFromNetwork = forceNetwork) + ToDoAPI.getCourseTodos(canvasContext, adapter, params, callback) + } + + fun getCourseTodosAsync(canvasContext: CanvasContext, forceNetwork: Boolean) = apiAsync> { getCourseTodos(canvasContext, forceNetwork, it) } + fun mergeToDoUpcoming(todoList: List?, eventList: List?): List { val todos = todoList ?: emptyList() var events = eventList ?: emptyList() // Add all Assignment ids from todos val assignmentIds = - HashSet(todos.asSequence().filter { it.assignment != null }.map { it.assignment?.id }.toList()) + HashSet(todos.asSequence().filter { it.assignment != null }.map { it.assignment?.id }.toList()) // If the set contains any assignment ids from Upcoming, it's a duplicate events = events.filter { it.assignment?.id ?: -1 !in assignmentIds } // Return combined list, sorted by date val defaultDate = Date(0) - return (todos + events).sortedBy{ it.comparisonDate ?: defaultDate } + return (todos + events).sortedBy { it.comparisonDate ?: defaultDate } } } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/UserManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/UserManager.kt index 34e23029a5..a9d1a27562 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/UserManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/UserManager.kt @@ -279,4 +279,18 @@ object UserManager { UserAPI.getMissingSubmissions(forceNetwork, adapter, depaginatedCallback) } + fun getTeacherListForCourseAsync(courseId: Long, forceNetwork: Boolean) = apiAsync> { getTeacherListForCourse(courseId, it, forceNetwork) } + + private fun getTeacherListForCourse(courseId: Long, callback: StatusCallback>, forceNetwork: Boolean) { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + val adapter = RestBuilder(callback) + val depaginatedCallback = object : ExhaustiveListCallback(callback) { + override fun getNextPage(callback: StatusCallback>, nextUrl: String, isCached: Boolean) { + UserAPI.getTeacherListForCourse(adapter, params, courseId, callback) + } + } + adapter.statusCallback = depaginatedCallback + UserAPI.getTeacherListForCourse(adapter, params, courseId, depaginatedCallback) + } + } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt index 0eb75ac813..4095fa0d94 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt @@ -103,7 +103,9 @@ data class Assignment( val externalToolAttributes: ExternalToolAttributes? = null, @SerializedName("planner_override") val plannerOverride: PlannerOverride? = null, - var isStudioEnabled: Boolean = false + var isStudioEnabled: Boolean = false, + @SerializedName("in_closed_grading_period") + val inClosedGradingPeriod: Boolean = false ) : CanvasModel() { override val comparisonDate get() = dueDate override val comparisonString get() = dueAt diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Course.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Course.kt index d526a25dda..e7a340c092 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Course.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Course.kt @@ -73,7 +73,9 @@ data class Course( @SerializedName("homeroom_course") val homeroomCourse: Boolean = false, @SerializedName("course_color") - val courseColor: String? = null + val courseColor: String? = null, + @SerializedName("grading_periods") + val gradingPeriods: List? = null ) : CanvasContext(), Comparable { override val type: Type get() = Type.COURSE diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/LTITool.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/LTITool.kt index 6d4a217758..ee4f2e039e 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/LTITool.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/LTITool.kt @@ -17,15 +17,31 @@ package com.instructure.canvasapi2.models +import android.os.Parcelable +import com.google.gson.annotations.SerializedName import kotlinx.android.parcel.Parcelize @Parcelize data class LTITool( - override var id: Long = 0, - var name: String? = null, - var url: String? = null, - var assignmentId: Long = 0L, - var courseId: Long = 0L + override var id: Long = 0, + var name: String? = null, + var url: String? = null, + var assignmentId: Long = 0L, + var courseId: Long = 0L, + @SerializedName("course_navigation") + var courseNavigation: CourseNavigation? = null, + @SerializedName("icon_url") + var iconUrl: String? = null, + @SerializedName("context_id") + var contextId: Long? = null, + @SerializedName("context_name") + var contextName: String? = null ) : CanvasModel() { override val comparisonString get() = name } + +@Parcelize +data class CourseNavigation( + val text: String? = null, + val url: String? = null +) : Parcelable diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Plannable.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Plannable.kt index 6f01ef1391..2d08118fa3 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Plannable.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Plannable.kt @@ -40,5 +40,8 @@ data class Plannable( // Used to determine if a quiz is an assignment or not @SerializedName("assignment_id") - val assignmentId: Long? -) {} + val assignmentId: Long?, + + @SerializedName("todo_date") + val todoDate: String? +) diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/PlannerItem.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/PlannerItem.kt index e7744b7aaf..8a53a647c6 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/PlannerItem.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/PlannerItem.kt @@ -35,7 +35,7 @@ data class PlannerItem ( val contextName: String?, @SerializedName("plannable_type") - val plannableType: String, + val plannableType: PlannableType, val plannable: Plannable, @@ -46,7 +46,13 @@ data class PlannerItem ( val htmlUrl: String?, @SerializedName("submissions") - val submissionState: SubmissionState? + val submissionState: SubmissionState?, + + @SerializedName("new_activity") + val newActivity: Boolean?, + + @SerializedName("planner_override") + var plannerOverride: PlannerOverride? = null ) { val canvasContext: CanvasContext @@ -61,3 +67,22 @@ data class PlannerItem ( } } + +enum class PlannableType { + @SerializedName("announcement") + ANNOUNCEMENT, + @SerializedName("assignment") + ASSIGNMENT, + @SerializedName("discussion_topic") + DISCUSSION_TOPIC, + @SerializedName("quiz") + QUIZ, + @SerializedName("wiki_page") + WIKI_PAGE, + @SerializedName("planner_note") + PLANNER_NOTE, + @SerializedName("calendar_event") + CALENDAR_EVENT, + @SerializedName("todo") + TODO +} diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/PlannerOverride.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/PlannerOverride.kt index f8777273d3..9ad7e9c450 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/PlannerOverride.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/PlannerOverride.kt @@ -22,6 +22,14 @@ import kotlinx.android.parcel.Parcelize @Parcelize data class PlannerOverride( - @SerializedName("dismissed") - val dismissed: Boolean = false + @SerializedName("id") + val id: Long? = null, + @SerializedName("plannable_type") + val plannableType: PlannableType, + @SerializedName("plannable_id") + val plannableId: Long, + @SerializedName("dismissed") + val dismissed: Boolean = false, + @SerializedName("marked_complete") + val markedComplete: Boolean = false ) : Parcelable \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/SubmissionState.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/SubmissionState.kt index e34a6fcc2a..3d3f5bb980 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/SubmissionState.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/SubmissionState.kt @@ -28,5 +28,17 @@ data class SubmissionState( @SerializedName("submitted") val submitted: Boolean = false, @SerializedName("missing") - val missing: Boolean = false + val missing: Boolean = false, + @SerializedName("late") + val late: Boolean = false, + @SerializedName("excused") + val excused: Boolean = false, + @SerializedName("graded") + val graded: Boolean = false, + @SerializedName("needs_grading") + val needsGrading: Boolean = false, + @SerializedName("with_feedback") + val withFeedback: Boolean = false, + @SerializedName("redo_request") + val redoRequest: Boolean = false ) \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/TermsOfService.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/TermsOfService.kt index 8111e98d57..29f83898d2 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/TermsOfService.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/TermsOfService.kt @@ -28,5 +28,13 @@ data class TermsOfService( val passive: Boolean = false, @SerializedName("account_id") val accountId: Long = 0, - val content: String? = null + val content: String? = null, + @SerializedName("self_registration_type") + val selfRegistrationType: SelfRegistration? = null ) : Parcelable + +enum class SelfRegistration(val apiString: String) { + @SerializedName("all") ALL("all"), + @SerializedName("observer") OBSERVER("observer"), + @SerializedName("none") NONE("none"), +} diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ApiPrefs.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ApiPrefs.kt index b952c5f9b6..5906538cfa 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ApiPrefs.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ApiPrefs.kt @@ -86,6 +86,9 @@ object ApiPrefs : PrefManager(PREFERENCE_FILE_NAME) { internal var masqueradeDomain by StringPref() internal var masqueradeUser: User? by GsonPref(User::class.java, null, "masq-user") + // Used to determine if a student can generate a pairing code, saved during splash + var canGeneratePairingCode by NBooleanPref() + var domain: String get() = if (isMasquerading || isStudentView) masqueradeDomain else originalDomain set(newDomain) { diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/CanvasApiExtensions.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/CanvasApiExtensions.kt index d0b53c6868..abc7fd1665 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/CanvasApiExtensions.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/CanvasApiExtensions.kt @@ -23,7 +23,7 @@ import java.text.SimpleDateFormat import java.util.* @JvmOverloads -fun Date?.toApiString(timeZone: TimeZone? = null): String? { +fun Date?.toApiString(timeZone: TimeZone? = null): String { this ?: return "" val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US) diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/DateHelper.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/DateHelper.kt index 104f0e75b0..4451c9eca8 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/DateHelper.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/DateHelper.kt @@ -211,9 +211,9 @@ object DateHelper { val cal = GregorianCalendar() cal.timeInMillis = dateTime val genericDate = GregorianCalendar( - cal[Calendar.YEAR], - cal[Calendar.MONTH], - cal[Calendar.DAY_OF_MONTH] + cal[Calendar.YEAR], + cal[Calendar.MONTH], + cal[Calendar.DAY_OF_MONTH] ) return Date(genericDate.timeInMillis) } @@ -233,4 +233,5 @@ object DateHelper { calendar[year, month, day, hour, minute] = second return calendar.time } + } diff --git a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/pact/canvas/apis/CoursesApiPactTests.kt b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/pact/canvas/apis/CoursesApiPactTests.kt index fa74d9c9f9..fcc8b5f791 100644 --- a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/pact/canvas/apis/CoursesApiPactTests.kt +++ b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/pact/canvas/apis/CoursesApiPactTests.kt @@ -102,7 +102,7 @@ class CoursesApiPactTests : ApiPactTestBase() { //region Test grabbing all courses // - val allCoursesQuery = "include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=current_and_concluded" + val allCoursesQuery = "include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=completed&state[]=available" val allCoursesPath = "/api/v1/courses" val allCoursesFieldInfo = listOf( // Evidently, permissions info is *not* returned from this call, even though include[]=permissions is specified diff --git a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/unit/CourseTest.kt b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/unit/CourseTest.kt index bf1336cd1e..6e0cd09412 100644 --- a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/unit/CourseTest.kt +++ b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/unit/CourseTest.kt @@ -22,7 +22,6 @@ import com.instructure.canvasapi2.models.Enrollment import com.instructure.canvasapi2.models.Section import com.instructure.canvasapi2.models.Term import com.instructure.canvasapi2.utils.Logger -import com.instructure.canvasapi2.utils.isNotDeleted import org.junit.Assert.* import org.junit.Before import org.junit.Test @@ -563,22 +562,4 @@ class CourseTest { assertTrue(course.isBetweenValidDateRange()) } - - @Test - fun `Course is not deleted when workflow state is available`() { - val course = baseCourse.copy(workflowState = Course.WorkflowState.AVAILABLE) - assertTrue(course.isNotDeleted()) - } - - @Test - fun `Course is not deleted when workflow state is completed`() { - val course = baseCourse.copy(workflowState = Course.WorkflowState.COMPLETED) - assertTrue(course.isNotDeleted()) - } - - @Test - fun `Course is deleted when workflow state is deleted`() { - val course = baseCourse.copy(workflowState = Course.WorkflowState.DELETED) - assertFalse(course.isNotDeleted()) - } } \ No newline at end of file diff --git a/libs/interactions/src/main/java/com/instructure/interactions/Navigation.kt b/libs/interactions/src/main/java/com/instructure/interactions/Navigation.kt index 417b8337dc..cb4f8288ae 100644 --- a/libs/interactions/src/main/java/com/instructure/interactions/Navigation.kt +++ b/libs/interactions/src/main/java/com/instructure/interactions/Navigation.kt @@ -25,7 +25,6 @@ interface Navigation { val currentFragment: Fragment? fun popCurrentFragment() - fun updateCalendarStartDay() fun addBookmark() fun canBookmark(): Boolean diff --git a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginInitActivity.kt b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginInitActivity.kt index f1dc4e5650..6169fcbf6e 100644 --- a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginInitActivity.kt +++ b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginInitActivity.kt @@ -95,17 +95,16 @@ abstract class BaseLoginInitActivity : AppCompatActivity() { event.getContentIfNotHandled()?.let { if (it) { startApp() + // We only want to finish here on debug builds, our login bypass for UI testing depends + // on a function called by this class, which then finishes the activity. + // See loginWithToken() in Teacher's InitLoginActivity. + if (!isTesting) finish() } else { logout() } } }) } - - // We only want to finish here on debug builds, our login bypass for UI testing depends - // on a function called by this class, which then finishes the activity. - // See loginWithToken() in Teacher's InitLoginActivity. - if (!isTesting) finish() } else { Handler().postDelayed({ runOnUiThread { diff --git a/libs/pandares/src/main/res/drawable/bg_lti_app_card.xml b/libs/pandares/src/main/res/drawable/bg_lti_app_card.xml new file mode 100644 index 0000000000..9929e30ee5 --- /dev/null +++ b/libs/pandares/src/main/res/drawable/bg_lti_app_card.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/libs/pandares/src/main/res/drawable/ic_arrow_down.xml b/libs/pandares/src/main/res/drawable/ic_arrow_down.xml new file mode 100644 index 0000000000..c2786c4651 --- /dev/null +++ b/libs/pandares/src/main/res/drawable/ic_arrow_down.xml @@ -0,0 +1,9 @@ + + + diff --git a/libs/pandares/src/main/res/drawable/ic_book.xml b/libs/pandares/src/main/res/drawable/ic_book.xml new file mode 100644 index 0000000000..8a4b5180fd --- /dev/null +++ b/libs/pandares/src/main/res/drawable/ic_book.xml @@ -0,0 +1,10 @@ + + + diff --git a/libs/pandares/src/main/res/drawable/ic_chevron_down_small.xml b/libs/pandares/src/main/res/drawable/ic_chevron_down_small.xml new file mode 100644 index 0000000000..58a9d0e595 --- /dev/null +++ b/libs/pandares/src/main/res/drawable/ic_chevron_down_small.xml @@ -0,0 +1,9 @@ + + + diff --git a/libs/pandares/src/main/res/drawable/ic_dashboard_grades.xml b/libs/pandares/src/main/res/drawable/ic_dashboard_grades.xml index f98c8b0515..46b622bd18 100644 --- a/libs/pandares/src/main/res/drawable/ic_dashboard_grades.xml +++ b/libs/pandares/src/main/res/drawable/ic_dashboard_grades.xml @@ -1,4 +1,4 @@ - + android:height="25dp" + android:viewportWidth="24" + android:viewportHeight="25"> - diff --git a/libs/pandares/src/main/res/drawable/ic_jump_to_today.xml b/libs/pandares/src/main/res/drawable/ic_jump_to_today.xml new file mode 100644 index 0000000000..2cc4087ebd --- /dev/null +++ b/libs/pandares/src/main/res/drawable/ic_jump_to_today.xml @@ -0,0 +1,10 @@ + + + diff --git a/libs/pandares/src/main/res/drawable/ic_mail.xml b/libs/pandares/src/main/res/drawable/ic_mail.xml new file mode 100644 index 0000000000..8dfced5d40 --- /dev/null +++ b/libs/pandares/src/main/res/drawable/ic_mail.xml @@ -0,0 +1,10 @@ + + + diff --git a/libs/pandares/src/main/res/drawable/ic_resources.xml b/libs/pandares/src/main/res/drawable/ic_resources.xml index abfe782df2..282ffb64df 100644 --- a/libs/pandares/src/main/res/drawable/ic_resources.xml +++ b/libs/pandares/src/main/res/drawable/ic_resources.xml @@ -1,4 +1,4 @@ - + android:height="25dp" + android:viewportWidth="24" + android:viewportHeight="25"> diff --git a/libs/pandares/src/main/res/drawable/ic_warning_red.xml b/libs/pandares/src/main/res/drawable/ic_warning_red.xml new file mode 100644 index 0000000000..d4ce172d36 --- /dev/null +++ b/libs/pandares/src/main/res/drawable/ic_warning_red.xml @@ -0,0 +1,10 @@ + + + diff --git a/libs/pandares/src/main/res/values-ar/strings.xml b/libs/pandares/src/main/res/values-ar/strings.xml index f9772f4ebc..69f58d9a2e 100644 --- a/libs/pandares/src/main/res/values-ar/strings.xml +++ b/libs/pandares/src/main/res/values-ar/strings.xml @@ -1053,6 +1053,7 @@ تعذر الإقران. تأكد من أن رمز الإقران صحيح وفي إطار الحد الزمني للاستخدام. مكتمل غير مكتمل + نشر الإعدادات نشر التقادير @@ -1230,24 +1231,47 @@ عرض إعلانات سابقة فشل تحميل جلسة التسجيل فشل تحديث جلسة التسجيل - لا شيء مستحق اليوم %1$s مستحق اليوم %1$s مفقود مرحبًا! تظهر الموضوعات هنا. ليس لديك موضوعات في الوقت الحالي. - - + يتطلب إعادة تشغيل التطبيق + تحديد + تحديد فترة التقييم + فترة التقدير الحالية + فشل تحميل الدرجات + تعذر تحميل التقديرات لفترة التقدير + تعذر تحديث التقديرات + لا توجد تقديرات للعرض + لم يتم التقييم + تغيير فترة التقدير + التقديرات غير متاحة + جلسة التسجيل + + عرض جلسة التسجيل + الروابط الهامة + تقديمات الطالب + معلومات الاتصال الخاصة بالعاملين + اختر مساقًا + الروابط الهامة + المعلم + المعلم المساعد + مواردك تظهر هنا. + فشل تحميل الموارد + فشل تحديث الموارد فتح طريقة عرض بديلة أكثر سهولة المتلقون الموضوع حدد مساقًا، %s + استجابات هذا الطالب مخفية لأن هذه المهمة مجهولة الاسم. التعليق التوضيحي للطالب (غير مدعوم) التعليق التوضيحي للطالب غير مدعوم حاليًا على الأجهزة المحمولة. نوع إرسال غير مدعوم لا يمكن عرض الإرسال، لأن التعليق التوضيحي للطالب غير مدعوم حاليًا على الأجهزة المحمولة. + قمت بتمييزه بأنه تم. diff --git a/libs/pandares/src/main/res/values-b+da+instk12/strings.xml b/libs/pandares/src/main/res/values-b+da+instk12/strings.xml index c473167d3c..851c6c2122 100644 --- a/libs/pandares/src/main/res/values-b+da+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+da+instk12/strings.xml @@ -1176,24 +1176,47 @@ Se tidligere beskeder Kunne ikke indlæse Homeroom Kunne ikke opdatere Homeroom - Intet forfalder i dag %1$s forfalder i dag %1$s mangler Velkommen! Dine emner vises her. Du har i øjeblikket ingen emner. - - + Kræver genstart af app + Vælg + Vælg vurderingsperiode + Nuværende vurderingsperiode + Kunne ikke indlæse vurderinger + Kunne ikke indlæse vurderinger for vurderingsperioden + Kunne ikke opdatere vurderinger + Ingen vurderinger at vise + Ikke bedømt + Skift vurderingsperiode + Vurderinger ikke tilgængelige + Klasselokale + + Klasseværelsevisning + Vigtige links + Applikationer for elev + Kontaktoplysninger for personale + Vælg et fag + Vigtige links + Lærer + Undervisningsassistent + Dine ressourcer vises her. + Kunne ikke indlæse ressourcer + Kunne ikke opdatere ressourcer Åbn en mere tilgængelig alternativ visning Modtagere Emne Vælg et fag, %s + Elevens svar er skjult, fordi denne opgave er anonym. Elevers anmærkninger (ikke understøttet) Elevers anmærkninger understøttes i øjeblikket ikke på mobil. Ikke-understøttet afleveringsform Aflevering kan ikke vises, fordi elevers anmærkninger i øjeblikket ikke understøttes på mobil. + Du har markeret det som udført. diff --git a/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml b/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml index 80e5407956..7eb9f67485 100644 --- a/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml +++ b/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml @@ -1176,24 +1176,47 @@ View Previous Announcements Failed to load Homeroom Failed to refresh Homeroom - Nothing Due Today %1$s due today %1$s missing Welcome! Your topics show up here. You currently have no topics. - - + Requires app restart + Select + Select Grading Period + Current Grading Period + Failed to load grades + Failed to load grades for grading period + Failed to refresh grades + No grades to display + Not Graded + Change grading period + Grades not available + Homeroom + + Homeroom View + Important Links + Student Applications + Staff Contact Info + Choose a Subject + Important Links + Instructor + Tutor + Your resources show up here. + Failed to load resources + Failed to refresh resources Open a more accessible alternative view Recipients Topic Select a subject, %s + This student\'s responses are hidden because this assignment is anonymous. Student Annotation (Unsupported) Student Annotation is currently not supported on mobile. Unsupported Submission Type Submission cannot be displayed, because Student Annotation is currently not supported on mobile. + You\'ve marked it as done. diff --git a/libs/pandares/src/main/res/values-b+nb+instk12/strings.xml b/libs/pandares/src/main/res/values-b+nb+instk12/strings.xml index 51333ed0fa..abf4ccb63d 100644 --- a/libs/pandares/src/main/res/values-b+nb+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+nb+instk12/strings.xml @@ -75,8 +75,8 @@ Vis Legg til fil valg Lydopptak Ta opp video - Redigeringsprogram for tekstinnlevering + Skriv… Noe gikk galt ved opplasting av innlevering. Lever på nytt. @@ -100,10 +100,10 @@ Denne oppgaven tillater ikke levering på nett Denne oppgaven tillater ikke levering på nett Ingen levering på nett - Denne oppgaven er lenket til et eksternt verktøy for innleveringer. Åpne verktøy Innleveringstekst + Av %s poeng Av %s poeng @@ -112,10 +112,10 @@ %1$s/%2$s %1$s av %2$s poeng Skriv inn what-If resultat - Lav: %s Gjennomsnitt: %s Høy: %s + Egendefinert resultat Det er ingen vurderingskriterier for denne oppgaven @@ -199,7 +199,6 @@ Ingen oppgaver i denne gruppen Denne oppgaven er fritatt og blir ikke vurdert i den totale beregningen EX - Sorter etter Tid Skriv inn @@ -211,6 +210,7 @@ Sorter oppgaver-knapp, sorter etter type Oppgaver sortert etter tid Oppgaver sortert etter type + Poeng\u0020 Lagre @@ -218,9 +218,9 @@ Svarene er kun synlige for dem som har postet minst ett svar. - Denne filen er for tiden låst Redigeringsprogram for diskusjonssvar + Starter Slutter @@ -1164,6 +1164,7 @@ Fjern alle fra oversikt Legg til oversikt Legg til alle til oversikt + Konto Hjem @@ -1172,28 +1173,50 @@ Ressurser Velkommen, %1$s! Mine emner - Vis tidligere beskjeder Kunne ikke laste inn Homeroom Kunne ikke oppdatere Homeroom - - Ingenting forfaller i dag %1$s forfaller i dag %1$s mangler Velkommen! Fagene dine vises her. Du har for øyeblikket ingen fag. - - + Krever omstart av appen + Velg + Velg vurderingsperiode + Gjeldende vurderingsperiode + Kunne ikke laste vurderinger + Kunne ikke laste inn vurderinger for vurderingsperioden + Kunne ikke oppdatere vurderinger + Ingen vurderinger å vise + Ikke vurdert + Endre vurderingsperiode + Vurderingerer er ikke tilgjengelig + Hjem + + Vis hjemmerom + Viktige lenker + Elev-applikasjoner + Kontaktinformasjon personale + Velg et fag + Viktige lenker + Lærer + Lærerassistent + Ressusrene dine vises her. + Kunne ikke laste opp ressurser + Kunne ikke oppdatere ressurser Åpne en mer tilgjengelig alternativ visning + Mottakere Tittel Velg et fag, %s + Denne elevens svar er skjult fordi denne oppgaven er anonym. Elevmerknad (ikke støttet) Elevmerknader støttes for øyeblikket ikke på mobil. Ustøttet innleveringstype Innlevering kan ikke vises fordi Elevmerknader for øyeblikket ikke støttes på mobil. + Du har merket det som fullført. diff --git a/libs/pandares/src/main/res/values-b+sv+instk12/strings.xml b/libs/pandares/src/main/res/values-b+sv+instk12/strings.xml index d5334b75af..c360246ff2 100644 --- a/libs/pandares/src/main/res/values-b+sv+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+sv+instk12/strings.xml @@ -75,8 +75,8 @@ Visa alternativ för Lägg till fil Spela in ljud Spela in video - Textinlämningsredigerare + Skriv… Något gick fel vid uppladdning av inlämningen. Lämna in igen. @@ -100,10 +100,10 @@ Den här uppgiften tillåter inte onlineinlämningar Den här uppgiften tillåter inte onlineinlämningar Inga onlineinlämningar - Den här uppgiften länkar till ett externt verktyg för inlämningar. Öppna verktyg Inlämningstext + Av %s poäng Av %s poäng @@ -112,10 +112,10 @@ %1$s/%2$s %1$s av %2$s poäng Ange Vad om-resultat - Låg: %s Medel: %s Hög: %s + Anpassat resultat Det finns ingen matris för den här uppgiften @@ -127,7 +127,7 @@ Laddar upp mediefil Uppladdningen av kommentaren misslyckades för %s Bifoga filer till dina kommentarer genom att välja ett alternativ nedan - Har du frågor om din uppgift?\nSkicka ett meddelande till din instruktör. + Har du frågor om din uppgift?\nSkicka ett meddelande till din lärare. Detta meddelande kunde inte skickas. Tryck för att försöka igen. Mediauppladdning Mediauppladdning – ljud @@ -199,7 +199,6 @@ Inga uppgifter i den här gruppen Den här uppgiften är ursäktad och kommer inte räknas in i den totala uträkningen EX - Sortera efter Tid Typ @@ -211,6 +210,7 @@ Sortera uppgiftsknappar, sortera efter typ Uppgifter sorterade efter tid Uppgifter sorterade efter typ + Poäng\u0020 Spara @@ -218,9 +218,9 @@ Svaren är endast synliga för de som har publicerat minst ett svar. - Den här filen är för närvarande låst Diskussionssvarsredigerare + Börjar Slutar @@ -339,7 +339,7 @@ Öppna Öppna med en alternativ app Öppnar fil… - Ladda ned + Ladda ner Meddelandebilagor Bilaga Bilageikon @@ -355,7 +355,7 @@ Välj en kurs eller grupp Inga meddelanden Ta bort bilaga - Ladda ned bilaga + Ladda ner bilaga Meddelandealternativ Vidarebefordra Svara alla @@ -448,6 +448,7 @@ Sluta uppträda som användare Användar-ID Ett fel inträffade vid försök att uppträda som användaren. + ID kan inte lämnas tomt Gå till quiz @@ -604,6 +605,8 @@ Övningsquiz Bedömda enkäter Enkäter + + Att göra Omdömen @@ -649,12 +652,12 @@ Profilinställningar Kontoinställningar PIN och fingeravtryck - Koppla med observatör Be din förälder scanna QR-koden från Canvas Parent-appen för att parkopplas med dig. Den här koden upphör att gälla om sju dagar, eller efter en användning. Kopplingskod:\u0020 Parkopplingskodfel Det gick inte att hämta en parkopplingskod. Den här funktionen stöds endast för elever. + Öppna Speedgrader att göra att göra lista @@ -725,8 +728,8 @@ SpeedGrader Mätare - Bedömningsreglage + Skapa ny händelse Stäng av pandorna @@ -744,7 +747,7 @@ Kursaktiviteter Diskussioner - Konversationer + Inkorgen Schemaläggning Grupper Larm @@ -766,7 +769,7 @@ Diskussionsinlägg Lägg till i konversation - Konversationsmeddelande + Meddelande i Inkorgen Konversationer som skapats av dig Elevens mötesregistreringar @@ -795,24 +798,25 @@ Få aviseringar när du skapar ett anslag, och när någon svarar på ditt anslag. Få aviseringar när en uppgift/inlämning har bedömts/ändrats, och när en omdömesvikt ändrats. Få aviseringar om inbjudningar till webbkonferenser, grupper, samarbeten, kamratresponsuppgifter och påminnelser. - Endast lärare och admin. Få aviseringar när en uppgift lämnas in för första gången eller lämnas in på nytt. - Endast lärare och admin. Få aviseringar när en sen uppgift skickas in. + Endast lärare och administratörer. Få aviseringar när en uppgift lämnas in för första gången eller lämnas in på nytt. + Endast lärare och administratörer. Få aviseringar när en sen uppgift skickas in. Få aviseringar när en kommentar görs om din inlämning. Få aviseringar när det finns ett nytt diskussionsämne i din kurs. Få aviseringar när det finns ett nytt inlägg i en diskussion som du prenumererar på. Få aviseringar när du läggs till i en konversation. Få aviseringar när du har ett nytt meddelande i din inkorg. Få aviseringar när du skapar en ny konversation. - Endast lärare och admin. Få aviseringar när det finns en mötesregistrering. + Endast lärare och administratörer. Få aviseringar när det finns en mötesregistrering. Få aviseringar när det finns en ny registrering på din kalender. Få aviseringar vid avbeställning av tid. Få aviseringar när en möteslucka blir tillgänglig. Få aviseringar om nya och uppdaterade kalenderobjekt. Endast admin, väntande registrering aktiverad. Få avisering när en gruppregistrering accepteras eller nekas. - Endast lärare och admin. Få aviseringar om kursregistreringar, genererade rapporter, exporterat innehåll, migrationsrapporter, nya kontoanvändare, och nya elevgrupper. + Endast lärare och administratörer. Få aviseringar om kursregistreringar, genererade rapporter, exporterat innehåll, migrationsrapporter, nya kontoanvändare, och nya elevgrupper. Få aviseringar när en konferensinspelning är klar. Obegränsade + Fråga %d fråga @@ -823,6 +827,7 @@ %s poäng %s poäng + Tidsgräns %1$s Nya aviseringar gilla-markering @@ -994,10 +999,10 @@ Konferenser stöds ännu inte på den här mobilen. Förhandsvisningsbild för fil Ett fel uppstod vid inläsning av den här PDF:en. - Tyvärr! Den här funktionen tillåts inte i elevens vyn. Inget att se här Funktionen saknar stöd + Lägg till elev Ange kopplingskoden du fått för elever. @@ -1008,7 +1013,7 @@ Publiceringsinställningar - Offentliggör omdömen + Publicera omdömen Dölj omdömen %d omdöme publicerat för närvarande @@ -1030,16 +1035,16 @@ Alla omdömen har publicerats. Publicerade omdömen Dolda omdömen - Det gick inte att offentliggör omdömen + Det gick inte att publicera omdömen Det gick inte att dölja omdömen Bedöm före publicering Bedöm efter publicering Åsidosättning av omdöme Aktuell omdöme - Öppnas i Canvas Elev Elevens vy + Vald rubrik: %s Vald brödtext: %s @@ -1111,6 +1116,7 @@ %s. %s %s %s %s, %s + %s minut %s minuter @@ -1131,6 +1137,7 @@ Konferensinformation Inspelningar Konferens pågår... + Kriteriebedömning %s %s, %s mer information @@ -1157,6 +1164,7 @@ Ta bort alla från översikten Lägg till i översikten Lägga till alla i översikten + Konto Hemrum @@ -1165,28 +1173,50 @@ Resurser Välkommen %1$s! Mina ämnen - Visa tidigare meddelanden Det gick inte att läsa in fjärrundervisningsrummet. Det gick inte att uppdatera fjärrundervisningsrummet. - - Inget måste lämnas in i dag %1$s förfaller i dag %1$s saknas Välkommen! Dina ämnen visas här. Du har inga ämnen för närvarande. - - + Kräver omstart av appen + Välj + Välj bedömningsperiod + Aktuell bedömningsperiod + Det gick inte att läsa in omdömen + Det gick inte att läsa in bedömningar för bedömningsperioden + Det gick inte att uppdatera bedömningar + Det finns inga bedömningar att visa + Inte bedömd + Ändra bedömningsperiod + Bedömningar är inte tillgängliga + Hemrum + + Vy för fjärrundervisningsrum + Viktiga länkar + Elev-applikationer + Kontaktuppgifter för personal + Välj en kurs + Viktiga länkar + Lärare + Lärarassistent + Dina resurser visas här. + Det gick inte att läsa in resurser + Det gick inte att uppdatera resurser Öppna en mer tillgänglig alternativ vy + Mottagare Ämne Välj en kurs, %s + Den här elevens svar är dolda eftersom uppgiften är anonym. Elevnotering (stöds inte) Elevnotering stöds för närvarande inte på mobila enheter. Inlämningstyp som inte stöds Inlämningen kan inte visas eftersom elevnotering inte stöds på mobila enheter. + Du har markerat den som färdig. diff --git a/libs/pandares/src/main/res/values-ca/strings.xml b/libs/pandares/src/main/res/values-ca/strings.xml index f60268850c..1bc293502c 100644 --- a/libs/pandares/src/main/res/values-ca/strings.xml +++ b/libs/pandares/src/main/res/values-ca/strings.xml @@ -1176,24 +1176,47 @@ Visualitza els anuncis anteriors No s\'ha pogut carregar la tutoria No s\'ha pogut actualitzar la tutoria - Res venç avui %1$s venç avui Falta %1$s Us donem la benvinguda! Les vostres matèries es mostren aquí. En aquest moment, no teniu cap matèria. - - + Heu de reiniciar l’aplicació. + Selecciona + Selecciona el període de qualificació + Període de qualificació actual + No s\'han pogut carregar les qualificacions + No s\'han pogut carregar les qualificacions del període de qualificació sol·licitat + No s\'han pogut actualitzar les qualificacions + No hi ha cap qualificació a mostrar + Sense qualificació + Canvia el període de qualificació + No hi cap qualificació disponible + Tutoria + + Vista de la tutoria. + Enllaços importants + Sol·licituds dels estudiants + Informació de contacte del personal + Tria un curs + Enllaços importants + Professor + Auxiliar de professor + Els vostres recursos es mostren aquí. + No s\'han pogut carregar els recursos + No s\'han pogut actualitzar els recursos Obriu una visualització alternativa més accessible Destinataris Assumpte Seleccioneu un curs, %s + Les respostes d\'aquest estudiant estan amagades perquè la tasca és anònima. Anotació de l’estudiant (no admesa) En aquest moment, no s\'admet l’anotació de l\'estudiant al dispositiu mòbil. Tipus de entrega no admès No es pot mostrar l’entrega, perquè en aquest moment no s\'admet l’anotació de l\'estudiant al dispositiu mòbil. + L’heu marcat com a fet. diff --git a/libs/pandares/src/main/res/values-cy/strings.xml b/libs/pandares/src/main/res/values-cy/strings.xml index 2dd5452e16..cba13db359 100644 --- a/libs/pandares/src/main/res/values-cy/strings.xml +++ b/libs/pandares/src/main/res/values-cy/strings.xml @@ -1176,24 +1176,47 @@ Gweld Cyhoeddiadau Blaenorol Wedi methu llwytho Homeroom Wedi methu adnewyddu Homeroom - Dim byd i fod i mewn heddiw %1$s i fod i mewn heddiw %1$s ar goll Croeso! Mae eich pynciau’n ymddangos yma. Does gennych chi ddim pynciau ar hyn o bryd. - - + Mae angen ailddechrau’r ap + Dewiswch + Dewis Cyfnod Graddio + Cyfnod Graddio Presennol + Wedi methu llwytho’r graddau + Wedi methu llwytho’r graddau ar gyfer y cyfnod graddio + Wedi methu adnewyddu’r graddau + Dim Graddau i’w dangos + Heb eu graddio + Newid cyfnod graddio + Graddau ddim ar gael + Ystafell hafan + + Gwedd Homeroom + Dolenni Pwysig + Ceisiadau Myfyriwr + Manylion Cyswllt Staff + Dewis Cwrs + Dolenni Pwysig + Athro + Cynorthwyydd Dysgu + Mae eich adnoddau’n ymddangos yma. + Wedi methu llwytho adnoddau + Wedi methu adnewyddu’r adnoddau Agor gwedd arall fwy hygyrch Derbynwyr Pwnc Dewiswch gwrs, %s + Mae atebion y myfyriwr hwn wedi’u cuddio gan fod yr aseiniad hwn yn ddienw. Anodiad gan Fyfyriwr (Anghydnaws) Does dim modd delio ag anodiadau gan fyfyrwyr ar ddyfeisiau symudol ar hyn o bryd. Math o Gyflwyniad Anghydnaws Does dim modd dangos y cyflwyniad, oherwydd does dim modd delio ag anodiadau gan fyfyrwyr ar ddyfeisiau symudol ar hyn o bryd. + Rydych chi wedi’i farcio fel wedi’i orffen. diff --git a/libs/pandares/src/main/res/values-da/strings.xml b/libs/pandares/src/main/res/values-da/strings.xml index 86a652e81f..0d6821981e 100644 --- a/libs/pandares/src/main/res/values-da/strings.xml +++ b/libs/pandares/src/main/res/values-da/strings.xml @@ -1176,24 +1176,47 @@ Se tidligere beskeder Kunne ikke indlæse Homeroom Kunne ikke opdatere Homeroom - Intet forfalder i dag %1$s forfalder i dag %1$s mangler Velkommen! Dine emner vises her. Du har i øjeblikket ingen emner. - - + Kræver genstart af app + Vælg + Vælg karakterperiode + Nuværende karakterperiode + Kunne ikke indlæse karakterer + Kunne ikke indlæse karakterer for karakterperioden + Kunne ikke opdatere karakterer + Ingen karakterer at vise + Ikke bedømt + Skift karakterperiode + Karakterer ikke tilgængelige + Klasselokale + + Klasseværelsevisning + Vigtige links + Applikationer for studerende + Kontaktoplysninger for personale + Vælg et fag + Vigtige links + Lærer + Undervisningsassistent + Dine ressourcer vises her. + Kunne ikke indlæse ressourcer + Kunne ikke opdatere ressourcer Åbn en mere tilgængelig alternativ visning Modtagere Emne Vælg et fag, %s + Den studerendes svar er skjult, fordi denne opgave er anonym. Studerendes anmærkninger (ikke understøttet) Studerendes anmærkninger understøttes i øjeblikket ikke på mobil. Ikke-understøttet afleveringsform Aflevering kan ikke vises, fordi studerendes anmærkninger i øjeblikket ikke understøttes på mobil. + Du har markeret det som udført. diff --git a/libs/pandares/src/main/res/values-de/strings.xml b/libs/pandares/src/main/res/values-de/strings.xml index ed05e5a20d..c31728f9f5 100644 --- a/libs/pandares/src/main/res/values-de/strings.xml +++ b/libs/pandares/src/main/res/values-de/strings.xml @@ -1176,24 +1176,47 @@ Vorherige Ankündigungen anzeigen Organisationsstunde konnte nicht geladen werden. Aktualisierung der Organisationsstunde fehlgeschlagen - Heute ist nichts fällig. %1$s heute fällig %1$s fehlt/fehlen Willkommen! Ihre Fächer werden hier angezeigt. Zurzeit haben Sie keine Themen. - - + Erfordert Neustart der App + Auswählen + Benotungszeitraum auswählen + Aktueller Benotungszeitraums + Laden von Noten fehlgeschlagen + Noten für den Benotungszeitraum konnten nicht geladen werden. + Noten konnten nicht aktualisiert werden. + Keine Noten anzuzeigen + Unbenotet + Benotungszeitraum ändern + Noten nicht verfügbar + Organisationsstunde + + Organisationsstundenansicht + Wichtige Links + Anwendungen für Studenten + Mitarbeiterkontakt-Info + Einen Kurs auswählen + Wichtige Links + Lehrer + Lehrassistent + Ihre Ressourcen werden hier angezeigt. + Ressourcen konnten nicht geladen werden + Ressourcen konnten nicht aktualisiert werden Eine leichter zugängliche, alternative Ansicht öffnen Empfänger Betreff Einen Kurs auswählen, %s + Die Antworten dieses Studenten sind ausgeblendet, weil diese Aufgabe anonym ist. Studentenanmerkung (Nicht unterstützt) Studentenanmerkungen werden derzeit auf Mobilgeräten nicht unterstützt. Nicht unterstützte Abgabeart Abgabe kann nicht angezeigt werden, weil Studentenanmerkungen derzeit auf Mobilgeräten nicht unterstützt werden. + Sie haben es es als erledigt markiert. diff --git a/libs/pandares/src/main/res/values-en-rAU/strings.xml b/libs/pandares/src/main/res/values-en-rAU/strings.xml index d81f164df8..e2fdc62e38 100644 --- a/libs/pandares/src/main/res/values-en-rAU/strings.xml +++ b/libs/pandares/src/main/res/values-en-rAU/strings.xml @@ -1176,24 +1176,47 @@ View Previous Announcements Failed to load Homeroom Failed to refresh Homeroom - Nothing Due Today %1$s due today %1$s missing Welcome! Your subjects show up here. You currently have no subjects. - - + Requires app restart + Select + Select Grading Period + Current Grading Period + Failed to load marks + Failed to load grades for grading period + Failed to refresh grades + No grades to display + Not Marked + Change grading period + Grades not available + Homeroom + + Homeroom View + Important Links + Student Applications + Staff Contact Info + Choose a Course + Important Links + Teacher + Teaching Assistant + Your resources show up here. + Failed to load resources + Failed to refresh resources Open a more accessible alternative view Recipients Subject Select a course, %s + This student\'s responses are hidden because this assignment is anonymous. Student Annotation (Unsupported) Student Annotation is currently not supported on mobile. Unsupported Submission Type Submission cannot be displayed, because Student Annotation is currently not supported on mobile. + You\'ve marked it as done. diff --git a/libs/pandares/src/main/res/values-en-rCA/strings.xml b/libs/pandares/src/main/res/values-en-rCA/strings.xml index 7e6db64d5a..4476d3a3a0 100644 --- a/libs/pandares/src/main/res/values-en-rCA/strings.xml +++ b/libs/pandares/src/main/res/values-en-rCA/strings.xml @@ -1183,6 +1183,29 @@ Welcome! Your subjects show up here. You currently have no subjects. + Requires app restart + Select + Select Grading Period + Current Grading Period + Failed to load grades + Failed to load grades for grading period + Failed to refresh grades + No grades to display + Not Graded + Change grading period + Grades not available + Homeroom + Homeroom View + Important Links + Student Applications + Staff Contact Info + Choose a Course + Important Links + Teacher + Teaching Assistant + Your resources show up here. + Failed to load resources + Failed to refresh resources Open a more accessible alternative view @@ -1196,4 +1219,5 @@ Student Annotation is currently not supported on mobile. Unsupported Submission Type Submission cannot be displayed, because Student Annotation is currently not supported on mobile. + You\'ve marked it as done. diff --git a/libs/pandares/src/main/res/values-en-rCY/strings.xml b/libs/pandares/src/main/res/values-en-rCY/strings.xml index ac9a3dd067..f0f137853f 100644 --- a/libs/pandares/src/main/res/values-en-rCY/strings.xml +++ b/libs/pandares/src/main/res/values-en-rCY/strings.xml @@ -1176,24 +1176,47 @@ View Previous Announcements Failed to load Homeroom Failed to refresh Homeroom - Nothing Due Today %1$s due today %1$s missing Welcome! Your subjects show up here. You currently have no subjects. - - + Requires app restart + Select + Select Grading Period + Current Grading Period + Failed to load grades + Failed to load grades for grading period + Failed to refresh grades + No grades to display + Not Graded + Change grading period + Grades not available + Homeroom + + Homeroom View + Important Links + Student Applications + Staff Contact Info + Choose a Module + Important Links + Teacher + Teaching Assistant + Your resources show up here. + Failed to load resources + Failed to refresh resources Open a more accessible alternative view Recipients Subject Select a module, %s + This student\'s responses are hidden because this assignment is anonymous. Student Annotation (Unsupported) Student Annotation is currently not supported on mobile. Unsupported Submission Type Submission cannot be displayed, because Student Annotation is currently not supported on mobile. + You\'ve marked it as done. diff --git a/libs/pandares/src/main/res/values-en-rGB/strings.xml b/libs/pandares/src/main/res/values-en-rGB/strings.xml index 5079ad983c..1b6933e87a 100644 --- a/libs/pandares/src/main/res/values-en-rGB/strings.xml +++ b/libs/pandares/src/main/res/values-en-rGB/strings.xml @@ -1176,24 +1176,47 @@ View Previous Announcements Failed to load Homeroom Failed to refresh Homeroom - Nothing Due Today %1$s due today %1$s missing Welcome! Your subjects show up here. You currently have no subjects. - - + Requires app restart + Select + Select Grading Period + Current Grading Period + Failed to load grades + Failed to load grades for grading period + Failed to refresh grades + No grades to display + Not Graded + Change grading period + Grades not available + Homeroom + + Homeroom View + Important Links + Student Applications + Staff Contact Info + Choose a Course + Important Links + Teacher + Teaching Assistant + Your resources show up here. + Failed to load resources + Failed to refresh resources Open a more accessible alternative view Recipients Subject Select a course, %s + This student\'s responses are hidden because this assignment is anonymous. Student Annotation (Unsupported) Student Annotation is currently not supported on mobile. Unsupported Submission Type Submission cannot be displayed, because Student Annotation is currently not supported on mobile. + You\'ve marked it as done. diff --git a/libs/pandares/src/main/res/values-es/strings.xml b/libs/pandares/src/main/res/values-es/strings.xml index 56a8bddee8..610c7afa98 100644 --- a/libs/pandares/src/main/res/values-es/strings.xml +++ b/libs/pandares/src/main/res/values-es/strings.xml @@ -75,8 +75,8 @@ Mostrar opciones de Agregar archivo Grabar audio Grabar video - Editor de Entrega de Texto + Escriba… Algo salió mal al cargar la entrega. Entregar nuevamente. @@ -100,10 +100,10 @@ Esta tarea no permite entregas en línea Esta tarea no permite entregas en línea Sin entregas en línea - Esta tarea se vincula con una herramienta externa para entregas. Abrir herramienta Texto de la entrega + De %s puntos De %s pts @@ -112,10 +112,10 @@ %1$s/%2$s %1$s de %2$s puntos Ingresar puntaje hipotético - Bajo: %s Promedio: %s Alto: %s + Puntaje personalizado No hay ninguna rúbrica para esta tarea @@ -199,7 +199,6 @@ No hay tarea en este grupo Esta tarea está justificada y no será considerada en el cálculo total EX - Ordenar por Hora Tipo @@ -211,6 +210,7 @@ Botón Ordenar tareas, ordenar por tipo Tareas ordenadas por hora Tareas ordenadas por tipo + Puntos\u0020 Guardar @@ -218,9 +218,9 @@ Las respuestas son visibles solamente para aquellos que han publicado al menos una respuesta. - Este archivo está bloqueado actualmente Editor de Respuesta de Discusión + Comienza en Finaliza @@ -448,6 +448,7 @@ Dejar de actuar como usuario Identificación de usuario Hubo un error al intentar actuar como usuario + El identificador no puede estar en blanco Ir al examen @@ -604,6 +605,8 @@ Exámenes de práctica Encuestas calificadas Encuestas + + Por hacer Calificaciones @@ -649,12 +652,12 @@ Configuración del perfil Preferencias de la cuenta PIN y huella digital - Emparejamiento con un observador Pídale a su padre/madre que escanee este código QR desde la aplicación Canvas Parent para emparejarse con usted. Este código caducará dentro de siete días, o tras el primer uso. Código de emparejamiento:\u0020 Ocurrió un error con el código de emparejamiento No se pudo obtener un código de emparejamiento. Esta funcionalidad solo es compatible para los estudiantes. + Abrir SpeedGrader Lista de cosa o cosas por hacer @@ -725,8 +728,8 @@ SpeedGrader Indicador - Control deslizante de calificaciones + Crear nuevo evento Apagar los pandas @@ -813,6 +816,7 @@ Reciba notificaciones cuando esté lista la grabación de una conferencia. Ilimitados + Pregunta %d pregunta @@ -823,6 +827,7 @@ %s punto %s puntos + Límite de tiempo %1$s Nuevas notificaciones me gusta @@ -994,10 +999,10 @@ Los teléfonos móviles aún no admiten conferencias. Imagen de vista previa del archivo Se ha producido un error al intentar cargar este PDF. - ¡Lo sentimos! Esta funcionalidad no se permite en la vista del estudiante. No hay nada para ver aquí Funcionalidad no compatible + Agregar estudiante Introduzca el código de emparejamiento de estudiante que se le proporcionó. @@ -1036,10 +1041,10 @@ Calificación después de la publicación Anulación de las calificaciones Calificación actual - Se abre en Canvas Student Vista del estudiante + Cabeza seleccionada: %s Cuerpo seleccionado: %s @@ -1132,6 +1137,7 @@ Detalles de la conferencia Grabaciones Conferencia en curso + Valoración del criterio %s %s, %s más información @@ -1158,6 +1164,7 @@ Eliminar todo del Tablero Agregar al Tablero Agregar todo al Tablero + Cuenta Aula principal @@ -1166,28 +1173,50 @@ Recursos ¡Le damos la bienvenida, %1$s! Mis temas - Ver anuncios previos No se pudo cargar el Aula principal (Homeroom) No se pudo actualizar el Aula principal (Homeroom) - - Nada con fecha límite el día de hoy %1$s tiene fecha límite el día de hoy %1$s faltante ¡Bienvenido! Sus materias se muestran aquí. No tiene materias actualmente. - - + Requiere reiniciar la aplicación + Seleccionar + Seleccionar Período de Calificación + Período de Calificación Actual + No se pudieron cargar las calificaciones + No se pudieron cargar las calificaciones del período de calificación + No se pudieron actualizar las calificaciones + No hay calificaciones para mostrar + Sin calificar + Cambiar Período de calificación + No hay calificaciones disponibles + Aula principal + + Vista del aula + Enlaces importantes + Solicitudes del alumno + Información de contacto del personal + Elegir un curso + Enlaces importantes + Profesor + Profesor asistente + Sus recursos se muestran aquí. + No se pudieron cargar los recursos + No se pudieron actualizar los recursos Abrir una vista alternativa más accesible + Destinatarios Asunto Seleccione un curso, %s + Las respuestas del estudiante están ocultas porque esta tarea es anónima. Anotación del estudiante (no admitida) La anotación del estudiante actualmente no se admite en el móvil. Tipo de entrega no admitida La entrega no se puede mostrar porque la Anotación del estudiante actualmente no se admite en el móvil. + Lo marcó como terminado. diff --git a/libs/pandares/src/main/res/values-fi/strings.xml b/libs/pandares/src/main/res/values-fi/strings.xml index ded59a20a7..260a17bd02 100644 --- a/libs/pandares/src/main/res/values-fi/strings.xml +++ b/libs/pandares/src/main/res/values-fi/strings.xml @@ -75,8 +75,8 @@ Näytä Lisää tiedosto -asetukset Nauhoita ääni Nauhoita video - Tekstin lähetyksen editori + Kirjoita… Jotakin meni pieleen lähetyksen päivityksessä. Lähetä uudelleen. @@ -100,10 +100,10 @@ Tämä tehtävä ei salli verkkolähetyksiä. Tämä tehtävä ei salli verkkolähetyksiä. Ei verkkolähetyksiä - Tämä tehtävä linkittää ulkoisen työkalun lähetyksiin. Avaa työkalu Lähetyksen testi + /%s pisteestä /%s pistettä @@ -112,10 +112,10 @@ %1$s/%2$s %1$s/%2$s pistettä Syötä Mitä jos -pistemäärä. - Matala: %s Keskiarvo: %s Korkea: %s + Mukautettu pistemäärä Tälle tehtävälle ei ole rubriikkia @@ -199,7 +199,6 @@ Tässä ryhmässä ei ole tehtäviä Tämän tehtävän annetaan mennä eikä sitä oteta huomioon koko laskelmassa EX - Lajitteluperuste Aika Tyyppi @@ -211,6 +210,7 @@ Lajittele tehtävät -painike, lajittele tyypin perusteella Tehtävät lajiteltu ajan perusteella Tehtävät lajiteltu tyypin perusteella + Pisteitä\u0020 Tallenna @@ -218,9 +218,9 @@ Vastaukset ovat näkyvissä vai niille, jotka ovat lähettäneet vähintään yhden vastauksen. - Tämä tiedosto on parhaillaan lukittu Keskustelun lähetyksen editori + Alkaa Päättyy @@ -448,6 +448,7 @@ Lopeta toimiminen käyttäjänä Käyttäjätunnus Ilmeni virhe yritettäessä toimia tänä käyttäjänä. + Tunnus ei voi olla tyhjä. Siirry tietovisaan @@ -604,6 +605,8 @@ Harjoittelutietovisat Arvostellut kyselyt Kyselyt + + Tehtävä Arvosanat @@ -649,12 +652,12 @@ Profiiliasetukset Tilin asetukset PIN ja sormenjälki - Yhdistä havaitsijan kanssa Pyydä vanhempiasi skannaamaan tämä viivakoodi Canvas Parent -sovelluksesta, jotta he saavat yhteyden sovellukseen. Tämä koodi vanhenee seitsemässä päivässä tai yhden käytön jälkeen. Parinmuodostuskoodi:\u0020 Parimuodostuskoofin virhe Parinmuodostuskoodin nouto epäonnistui. Tämä ominaisuutta tuetaan vain opiskelijoille. + Avaa Speedgrader mitätehdä mitä tehdä mitä tehdä -luettelo @@ -725,8 +728,8 @@ SpeedGrader Mittari - Arvosanojen liukuasteikko + Luo uusi tapahtuma Kytke pandat pois päältä @@ -813,6 +816,7 @@ Saa ilmoitus, kun konferessin äänitys on valmis. Rajoittamaton + Kysymys %d kysymys @@ -823,6 +827,7 @@ %s piste %s pistettä + Aikaraja %1$s Uudet ilmoitukset tykkäys @@ -994,10 +999,10 @@ Konferensseja ei vielä tueta matkapuhelimessa. Tiedoston esikatselukuva Tämän PDF-tiedoston lataamisen yhteydessä ilmeni virhe. - Pahoittelut! Tätä ominaisuutta ei sallita opiskelijanäkymässä. Täällä ei ole mitään nähtävää. Tukematon ominaisuus + Lisää opiskelija Pane opiskelijan parinmuodostuskoodi, joka on toimitettu sinulle. @@ -1036,10 +1041,10 @@ Arvosana lähetyksen jälkeen Korvaa arvosana Nykyinen arvosana - Avautuu Canvas Studentissa Opiskelijanäkymä + Valittu pää: %s Valittu keho: %s @@ -1111,6 +1116,7 @@ %s. %s %s %s %s, %s + %s Minuutti %s minuuttia @@ -1131,6 +1137,7 @@ Konferenssin tiedot Nauhoitukset Kokous käynnissä + Kriteerien luokitukset %s %s, %s lisätietoja @@ -1157,6 +1164,7 @@ Poista widget koontinäytöstä Lisää koontinäytölle Lisää kaikki koontinäyttöön + Tili Homeroom @@ -1165,28 +1173,50 @@ Resurssit Tervetuloa, %1$s! Omat aiheet - Näytä edelliset ilmoitukset Homeroomin lataus epäonnistui Homeroomin päivitys epäonnistui - - Tänään ei eräänny mitään %1$s erääntyy tänään %1$s puuttuu Tervetuloa! Aiheesi näkyvät täällä. Sinulla ei tällä hetkellä ole aiheita. - - + Vaatii sovelluksen uudelleenkäynnistyksen + Valitse + Valitse arvosanojen antojakso + Nykyinen arvosanojen antojakso + arvosanojen lataus epäonnistui + Arvosanojen lataus pyydetylle arvosanojen antojaksolle epäonnistui + Arvosanojen päivitys epäonnistui + Ei esitettäviä arvosanoja + Ei arvioida + Vaihda arvosanojen antojakso + Arvosanat eivät ole käytettävissä + Homeroom + + Homeroom-näkymä + Tärkeitä linkkejä + Opiskeluhakemukset + Henkilökunnan yhteistiedot + Valitse kurssi + Tärkeitä linkkejä + Opettaja + Apuopettaja + Resurssisi näkyvät täällä. + Resurssien lataus epäonnistui + Resurssien päivitys epäonnistui Avaa lähestyttävämpi vaihtoehtoinen näkymä + Vastaanottajat Aihe Valitse kurssi, %s + Tämän opiskelijan vastaukset on kätketty, koska tehtävä on nimetön. Opiskelijan huomautusasiakirjat (tukematon) Opiskelijan huomautuksia ei tällä hetkellä tueta matkapuhelimessa. Lähetystyyppiä ei tueta Lähetystä ei voida näyttää, koska opiskelijan huomautuksia ei tällä hetkellä tueta matkapuhelimessa. + Olet merkinnyt sen tehdyksi. diff --git a/libs/pandares/src/main/res/values-fr-rCA/strings.xml b/libs/pandares/src/main/res/values-fr-rCA/strings.xml index 4bc07de3c7..e8a26f722b 100644 --- a/libs/pandares/src/main/res/values-fr-rCA/strings.xml +++ b/libs/pandares/src/main/res/values-fr-rCA/strings.xml @@ -55,10 +55,8 @@ Tentatives utilisées Aucune tentative restante Envoyer la tâche de nouveau - Lancement de l\'outil externe... Lancer l\'outil externe - Aperçu Le téléversement d\'un ou de plusieurs fichiers a échoué. Vérifiez votre connexion Internet et réessayez l’envoi. %1$s sur %2$s @@ -69,19 +67,20 @@ Envoi supprimé Manquant Noté + Aucun aperçu disponible pour les URL utilisant http:// Veuillez saisir une URL valide Saisissez un URL ici pour votre envoi Afficher les options d\'ajout de fichier Enregistrer le son - Enregistrer la vidéo - Éditeur d’envoi de texte + Écrire… Un problème est survenu lors du téléversement de l’envoi. Envoyer de nouveau. + Envoi Versions des envois @@ -104,6 +103,7 @@ Cette tâche comporte des liens vers un outil externe pour les envois. Ouvrir Outil Texte de dépôt + de %s points de %s pts @@ -115,10 +115,12 @@ Bas : %s Moyenne : %s Élevé : %s + Score personnalisé Il n\'y a aucune rubrique pour cette tâche. Afficher description longue + Téléversement du fichier %1$d sur %2$d Téléversement du commentaire pour %s @@ -138,6 +140,7 @@ Fichier multimédia Audio Vidéo + Version : @@ -187,6 +190,8 @@ Dû le %1$s à %2$s am pm + + Verrouillé Tâches @@ -205,15 +210,17 @@ Trier les boutons des tâches, trier par type Tâches triées par heure Tâches triées par type + Points\u0020 Enregistrer Annuler + Les réponses ne sont visibles que pour ceux qui ont publié au moins une réponse. - Ce fichier est présentement verrouillé Éditeur des réponses de discussion + Commence Se termine @@ -232,8 +239,6 @@ Score hypothétique %1$s/%2$s (%3$s) %1$s de %2$s points, %3$s - - Boîte de réception Non lu Archivé @@ -399,8 +404,8 @@ Étudiants Observateur Un programme n’a pas été ajouté. - Une erreur est survenue lors du chargement de vos modules. + Canvas Choisir un ou des destinataire(s) Ce message n\'a aucun destinataire actuellement @@ -426,6 +431,7 @@ Supprimé + Chargement du contenu de Canvas… UnknownDevice @@ -442,9 +448,11 @@ Arrêter d\'agir en tant qu\'utilisateur ID utilisateur Une erreur s\'est produite lors de la tentative d’agir en tant qu’utilisateur + L\'identifiant ne peut être vide Aller au questionnaire + Questionnaires Dernière publication @@ -503,8 +511,8 @@ Se rendre aux Modules Marquer comme terminé Élément de module introuvable - Une erreur est survenue lors du chargement de vos modules. + Aide Question de l\'instructeur Lien @@ -520,11 +528,11 @@ Saisie de texte URL en ligne Cet envoi n’accepte qu’un seul téléversement de fichier - - Ajouter des entrées du site Web Enregistrements multimédias Envoyer + + Progrès non sauvegardé Toute information non enregistrée sera perdue. Désirez-vous continuer? @@ -539,6 +547,7 @@ Suivant Ajouter un commentaire… + Accueil Notifications @@ -568,6 +577,8 @@ Un instantané du site a été capturé lorsque vous l\'avez rendu. Taper et maintenir l\'image ci-dessous pour ouvrir ou télécharger l\'image entière. Cet envoi correspond à une URL vers une page externe. Gardez à l’esprit que cette page peut avoir été modifiée depuis l’envoi d’origine. Un aperçu de l\'url soumise + + %1$s ne sont pas pris en charge. Le lien n\'est pas pris en charge. Ouvrir dans le navigateur @@ -583,15 +594,19 @@ Faits de panda :  Retirer Fondateurs + CLUF Politique de confidentialité Conditions d\'utilisation Canvas sur GitHub + Questionnaires de la tâche Questionnaires d\'entraînement Enquêtes notées Enquêtes + + À faire Notes @@ -637,12 +652,12 @@ Paramètres de profil Préférences du compte NIP et empreinte - Jumeler avec l’observateur Demandez à votre parent de numériser ce code QR à partir de l\'application Canvas Parent pour le jumeler avec vous. Ce code viendra à échéance dans sept jours, ou après une seule utilisation. Code de jumelage : \u0020 Erreur du jumelage de code Impossible de récupérer un code de jumelage. Cette fonctionnalité n\'est prise en charge que pour les étudiants. + Lancer Speedgrader tâche à faire tâches à faire liste des tâches à faire @@ -713,8 +728,8 @@ SpeedGrader Gauge - Curseur de notation + Créer un nouvel événement Désactiver les pandas @@ -801,6 +816,7 @@ Soyez informé lorsqu\'un enregistrement de conférence est prêt. Illimité + Question %d questions @@ -811,16 +827,17 @@ %s point %s points + Limite de temps %1$s Nouvelles notifications mention « J\'aime » Laisser une rétroaction de type « J’aime » à cette entrée mentions « J\'aime » - %s « j’aime » %s « j’aime » + Rouge Rose vif Lavande @@ -879,6 +896,7 @@ Modifier le tableau de bord Sélectionnez les cours que vous souhaitez voir sur le tableau de bord Modifier votre liste des cours + Tableau de bord Précédent Page %d sur %d @@ -906,8 +924,8 @@ Tapoter pour afficher l’annonce. Accepter Décliner - sur + Il n\'y a aucun fichier associé à ce cours. Il n\'y a aucun fichier associé à ce groupe. @@ -968,6 +986,7 @@ Nous n\'arrivons pas à trouver une application externe pour voit cet outil LTI. Téléversement de commentaire + Page de couverture Modifier une page Description @@ -980,10 +999,10 @@ Les conférences ne sont pas encore prises en charge sur mobile. Image de prévisualisation du fichier Une erreur s\'est produite en essayant de charger ce PDF. - Désolé! Cette fonctionnalité n\'est pas autorisée en mode Étudiant. Rien à voir ici Fonctionnalité non prise en charge + Ajouter un étudiant Saisir le code de jumelage de l’étudiant qui vous a été fourni. @@ -991,6 +1010,7 @@ Le jumelage a échoué. Assurez-vous que votre code de jumelage est adéquat et dans les délais d\'utilisation. Terminé Incomplet + Définitions de publication Publier les notes @@ -1021,10 +1041,10 @@ Notation après publication Surclasser la notation Note actuelle - S’ouvre dans Canvas Étudiant Vue étudiant + Entête choisi : %s Corps choisi : %s @@ -1078,7 +1098,6 @@ Ajouter un commentaire vidéo Ajouter un commentaire audio - Une erreur inattendue s\'est produite lors de l\'enregistrement du son. Une erreur inattendue s\'est produite lors de l\'enregistrement de la vidéo. @@ -1097,6 +1116,7 @@ %s. %s %s %s %s, %s + %s Minute %s Minutes @@ -1117,6 +1137,7 @@ Détails de la conférence Enregistrements Conférences en cours + Notes du critère %s %s, %s plus d\'informations @@ -1143,6 +1164,7 @@ Tout retirer du tableau de bord Ajouter au tableau de bord Tout ajouter au tableau de bord + Compte Classe titulaire @@ -1151,28 +1173,50 @@ Ressources Bienvenue, %1$s! Mes Objets - Voir les annonces précédentes Échec lors du chargement de la classe titulaire Échec lord de l\'actualisation de la classe titulaire - - Rien n’est dû aujourd’hui %1$s dû aujourd’hui %1$s manquant Bienvenue! Vos sujets apparaissent ici. Vous n’avez actuellement aucun sujet. - - + Redémarrage de l\'application requis + Sélectionner + Choisir la période de notation + Période de notation actuelle + Échec lors du chargement des notes + Échec lors du chargement des notes pour la période de notation + Échec lors de l’actualisation des notes + Aucune note à afficher + Non noté + Changer la période de notation + Notes non disponibles + Classe titulaire + + Vue de la salle d’accueil + Liens importants + Dossiers de l’étudiant + Coordonnées du personnel + Choisir un cours + Liens importants + Enseignant + Assistants d’enseignement + Vos ressources apparaissent ici. + Échec lors du chargement de ressource + Échec lors de l’actualisation des ressources Ouvrir une vue alternative plus accessible + Destinataires Objet Sélectionner un cours, %s + Les réponses de cet étudiant sont masquées du fait que cette tâche est anonyme. Annotation d’étudiant (non pris en charge) L’annotation d’étudiant n’est actuellement pas prise en charge sur les mobiles. Type d’envoi non pris en charge L’envoi ne peut pas être affiché, car l’annotation d’étudiant n’est actuellement pas prise en charge sur mobile. + Vous l’avez marqué comme terminé. diff --git a/libs/pandares/src/main/res/values-fr/strings.xml b/libs/pandares/src/main/res/values-fr/strings.xml index c9926e0756..0f40060f14 100644 --- a/libs/pandares/src/main/res/values-fr/strings.xml +++ b/libs/pandares/src/main/res/values-fr/strings.xml @@ -55,10 +55,8 @@ Tentatives utilisées Aucune tentative restante Soumettre à nouveau le travail - Lancement de l\'outil externe... Lancer l\'outil externe - Aperçu L’envoi d\'un ou plusieurs fichiers a échoué. Vérifiez l’état de votre connexion internet, puis réessayez. %1$s de %2$s @@ -69,19 +67,20 @@ Soumission supprimée Manquant Noté + Aucun aperçu disponible pour les URLs utilisant « http:// » Veuillez saisir une URL valide Entrez une URL ici pour votre soumission Afficher les options d\'ajout de fichier Enregistrer de l’audio - Enregistrer la vidéo - Éditeur de soumission de texte + Écrire… Un problème est survenu lors de l’envoi de la soumission. Relancez la soumission. + Soumission Versions de soumission @@ -104,6 +103,7 @@ Cette tâche renvoie à un outil externe pour les envois. Ouvrir outil Texte de la soumission + Sur %s points Sur %s pts @@ -115,10 +115,12 @@ Faible : %s Moyen : %s Élevé : %s + Score personnalisé Il n’existe aucun barème pour ce travail Afficher la description longue + Envoi du fichier %1$d sur %2$d Envoi du commentaire pour %s @@ -138,6 +140,7 @@ Fichier multimédia Audio Vidéo + Version : @@ -187,6 +190,8 @@ Dû le %1$s à %2$s am pm + + Verrouillé Devoirs @@ -205,15 +210,17 @@ Bouton de tri des travaux, tri par type Travaux triés par heure Travaux triés par type + Points\u0020 Enregistrer Annuler + Les réponses ne sont visibles que pour ceux qui ont publié au moins une réponse. - Ce fichier est actuellement verrouillé Éditeur de réponses de discussion + Démarre Prend fin @@ -232,8 +239,6 @@ Score hypothétique %1$s/%2$s (%3$s) %1$s sur %2$s points, %3$s - - Boîte de réception Non lu Archivé @@ -399,8 +404,8 @@ Élèves Observateurs Un programme n’a pu être chargé. - Une erreur est survenue durant le chargement de vos modules. + Canvas Choisir un ou des destinataire(s) Ce message n\'a aucun destinataire actuellement @@ -426,6 +431,7 @@ Supprimé + Chargement du contenu Canvas… UnknownDevice @@ -442,9 +448,11 @@ Cesser d\'agir en tant qu\'utilisateur ID utilisateur Une erreur est survenue lors de la tentative d\'agir au nom d’un utilisateur. + L\'identifiant ne peut être vierge Aller au questionnaire + Questionnaires Dernière publication @@ -503,8 +511,8 @@ Se rendre sur Modules Marquer comme terminé Élément de module introuvable - Une erreur est survenue durant le chargement de vos modules. + Aide Question de l\'instructeur Lien @@ -520,11 +528,11 @@ Saisie de texte URL en ligne Cette soumission n’accepte qu’un seul téléchargement de fichier - - Ajouter une entrée de site web Enregistrements multimédias Soumettre + + Progrès non sauvegardé Les informations non enregistrées seront perdues. Souhaitez-vous continuer ? @@ -539,6 +547,7 @@ Suivant Ajouter un commentaire… + Accueil Notifications @@ -568,6 +577,8 @@ Un instantané du site a été capturé lorsque vous l\'avez rendu. Taper et maintenir l\'image ci-dessous pour ouvrir ou téléverser l\'image entière. Cet envoi correspond à une URL vers une page externe. Gardez à l’esprit que cette page peut avoir été modifiée depuis l’envoi d’origine. Un aperçu de l\'url soumise + + %1$s n’est pas pris en chargé. Le lien n\'est pas supporté. Ouvrir dans le navigateur @@ -583,15 +594,19 @@ Le panda a à vous dire :  Supprimer Fondateurs + CGU Politique de confidentialité Conditions d\'utilisation Canvas sur GitHub + Quiz sous forme de devoir Quiz d’entraînement Enquêtes notées Sondages + + À faire Notes @@ -637,12 +652,12 @@ Paramètres de profil Préférences du compte PIN et empreinte digitale - Paire avec l\'observateur Faites scanner ce code QR par vos parents depuis l’application Canvas Parent pour les jumeler à vous. Ce code expirera dans sept jours, ou lorsqu\'il aura été utilisé une fois. Code de jumelage :\u0020 Erreur de code de jumelage Impossible de récupérer un code de jumelage. Cette fonctionnalité n’est disponible que pour les élèves. + Lancer Speedgrader tâche à faire tâches à faire liste des tâches à faire @@ -713,8 +728,8 @@ SpeedGrader Gauge - Curseur des notes + Créer un nouvel événement Désactiver les pandas @@ -801,6 +816,7 @@ Être notifié lorsqu’un nouvel enregistrement de conférence est prêt. Illimité + Question %d question @@ -811,16 +827,17 @@ %s point %s points + Limite de temps %1$s nouvelles notifications mention "J\'aime" Aimer l’entrée mentions "J\'aime" - %s like %s likes + Rouge Rose vif Lavande @@ -879,6 +896,7 @@ Modifier le tableau de bord Sélectionnez les cours que vous souhaitez voir sur le tableau de bord Modifier votre liste de cours + Tableau de bord Précédent Page %d sur %d @@ -906,8 +924,8 @@ Appuyez pour voir l’annonce Accepter Refuser - sur + Il n\'y a aucun fichier associé à ce cours. Il n\'y a aucun fichier associé à ce groupe. @@ -968,6 +986,7 @@ Nous n\'arrivons pas à trouver une application externe pour visualiser cet outil LTI. Téléchargement de commentaires + Première page Modifier la page Description @@ -980,10 +999,10 @@ Les conférences ne sont pas encore prises en charge sur mobile. Image de prévisualisation de fichier Une erreur est survenue lors du chargement de ce PDF. - Désolé ! Cette fonctionnalité n’est pas autorisée en vue « Élève » Rien à voir ici Fonction non prise en charge + Ajouter un étudiant Saisir le code de jumelage de l’étudiant qui vous a été fourni. @@ -991,6 +1010,7 @@ Le jumelage a échoué. Assurez-vous que votre code de jumelage est correct et dans les limites de durée d’utilisation prévues. Terminé Incomplet + Paramètres de publication Publier les notes @@ -1021,10 +1041,10 @@ Note après publication Remplacement de la note Note actuelle - S’ouvre dans Canvas Student Affichage élève + Tête sélectionnée : %s Corps sélectionné : %s @@ -1078,7 +1098,6 @@ Ajouter un commentaire vidéo Ajouter un commentaire audio - Une erreur inattendue s\'est produite lors de l\'enregistrement du son. Une erreur inattendue s\'est produite lors de l\'enregistrement vidéo. @@ -1097,6 +1116,7 @@ %s. %s %s %s %s, %s + %s Minute %s Minutes @@ -1117,6 +1137,7 @@ Détails de la conférence Enregistrements Conférence en cours + Notes du critère %s %s, %s Plus d\'informations @@ -1143,6 +1164,7 @@ Supprimer tout du tableau de bord Ajouter au tableau de bord Ajouter tout au tableau de bord + Compte Salle d\'accueil @@ -1151,28 +1173,50 @@ Ressources Bienvenue, %1$s ! Mes sujets - Afficher les annonces précédentes Impossible de charger la salle de classe Impossible de rafraîchir la salle de classe - - Rien à rendre aujourd\'hui %1$s à rendre aujourd\'hui %1$s manquant Bienvenue ! Vos sujets s\'affichent ici. Vous n\'avez actuellement aucun sujet. - - + Nécessite de redémarrer l’application + Sélectionner + Sélectionner la période de notation + Période de notation actuelle + Échec dans le chargement des notes + Échec du chargement des notes pour la période de notation + Échec de l\'actualisation des notes + Aucune note à afficher + Non noté + Modifier la période de notation + Notes indisponibles + Salle d\'accueil + + Vue de la classe d’attache + Liens importants + Applications des élèves + Coordonnées du personnel + Choisir un cours + Liens importants + Enseignant + Assistant d\'enseignement + Vos ressources s\'affichent ici + Échec du chargement des ressources + Échec de l\'actualisation des ressources Ouvrir une vue alternative plus accessible + Destinataires Sujet Sélectionner un cours, %s + Les réponses de cet élève sont masquées car ce travail est anonyme. Annotation de l\'élève (non prise en charge) L\'annotation des élèves n\'est actuellement pas prise en charge sur les téléphones mobiles. Type de soumission non pris en charge La soumission ne peut pas être affichée, car l\'annotation de l\'élève n\'est actuellement pas prise en charge sur les téléphones mobiles. + Vous avez tout indiqué comme terminé. diff --git a/libs/pandares/src/main/res/values-ht/strings.xml b/libs/pandares/src/main/res/values-ht/strings.xml index a2cadd52be..f0ba4c395e 100644 --- a/libs/pandares/src/main/res/values-ht/strings.xml +++ b/libs/pandares/src/main/res/values-ht/strings.xml @@ -55,10 +55,8 @@ Tantativ Itilize Pa Rete Tantativ Resoumèt Sesyon - Lansman Zouti Ekstèn Lanse Zouti Eksteryè - Apèsi Gen yonn oswa plizyè fichye pa rive transfere. Verifye koneksyon entènèt ou a epi eseye re voye yo ankò %1$s de %2$s @@ -69,19 +67,20 @@ Soumisyon Efase Manke Klase + Okenn apèsi disponib pou URL ki itilize \'http://\' Tanpri antre yon URL ki valid Antre yon URL la a pou soumisyon an Afiche Opsyon Ajoute Fichye Anrejistre Son - Anrejistre Videyo - Editè Soumisyon Tèks + Ekri… Gen yon bagay ki pase mal pandan soumisyon ap transfere. Soumèt ankò. + Soumisyon Vèsyon soumisyon @@ -104,6 +103,7 @@ Sesyon sa a relye a yon zouti ekstèn pou soumisyon. Ouvri Zouti Tèks soumisyon + Sou %s pwen Sou %s pts @@ -115,10 +115,12 @@ Ba: %s Mwayen: %s Wo: %s + Nòt pèsonalize Pa gen ribrik pou devwa sa a Afiche tout deskripsyon + Transfè fichye %1$d de %2$d Transfè Kòmantè pou %s @@ -138,6 +140,7 @@ Fichye Miltimedya Odyo Videyo + Vèsyon: @@ -187,6 +190,8 @@ Delè %1$s a %2$s am pm + + Bloke Sesyon @@ -205,15 +210,17 @@ Bouton pou triye devwa yo, triye pa tip Devwa triye pa lè Devwa triye pa tip + Pwen\u0020 Anrejistre Anile + Sèlman moun ki poste yon repons pou pi piti k ap ka wè repons yo. - Fichye sa a bloke pou kounye a Editè Repons Diskisyon + Kòmanse Fini @@ -232,8 +239,6 @@ Kisa-Si Nòt %1$s/%2$s (%3$s) %1$s sou %2$s pwen, %3$s - - Bwat resepsyon Poko li Achive @@ -399,8 +404,8 @@ Elèv Obsèvatè Pa gen yon pwogram ki ajoute. - Te gen yon erè pandan chajman modil ou yo. + Canvas Chwazi Destinatè... Mesaj sa a pa gen destinatè. @@ -426,6 +431,7 @@ Efase + Chajman Kontni Canvas… UnknownDevice @@ -442,9 +448,11 @@ Sispann Pase Pou Itilizatè ID Itilizatè Te gen erè pandan w ap eseye pase pou itilizatè + ID a pa kapab vid Ale nan Quiz + Quiz Dènye piblikasyon @@ -503,8 +511,8 @@ Ale nan Modil Make tankou li fini Eleman Modil Entwouvab - Te gen yon erè pandan chajman modil ou yo. + Èd Kesyon Enstriktè Lyen @@ -520,11 +528,11 @@ Antre Tèks URL an liy Soumisyon sa a aksepte transfè yon fichye sèlman - - Ajoute Antre Sit Anrejistreman Medya Soumèt + + Pwogrè non Anrejistre Enfòmasyon ki pa sovgade ap pèdi. Èske w vle kontinye? @@ -539,6 +547,7 @@ Pwochen Ajoute yon kòmantè… + Akèy Notifikasyon @@ -568,6 +577,8 @@ Yo te pran yon apèsi sit entènèt la lè w aktive li a. Tape epi kenbe imaj pi ba a pou ouvri oswa telechaje imaj la an antye. Soumisyon sa a se te yon URL a yon paj ekstèn. Sonje ke sa ka rive ke paj sa a chanje aprè soumisyon orijinal la. Yon apèsi url ki te soumèt la + + %1$s pa sipòte. Lyen an pa sipòte. Ouvri nan Navigatè @@ -583,15 +594,19 @@ Panda Fact:  Elimine Fondatè + EULA Politik Konfidansyalite Kondisyon Itilizasyon Canvas sou GitHub + Sesyon Quiz Quiz Pratik Ankèt Klase Ankèt + + Pou Fè Nòt @@ -637,12 +652,12 @@ Paramèt Pwofi Preferans Kont PIN ak Anprent - Asosye avèk Obsèvatè Fè paran w eskane Kòd QR sa a ak app Canvas Parent lan pou li kapab asosye ak ou. Kòd sa a ap ekspire nan sèt jou oswa aprè yon itilizasyon. Kòd Kouplaj:\u0020 Erè Kòd Kouplaj Enposib pou rekipere kòd kouplaj la. Fonksyon sa a se sèlman pou elèv. + Ouvri Speedgrader lis tout sa pou fè @@ -713,8 +728,8 @@ SpeedGrader Gauge - Ba klasman + Kreye Nouvo Aktivite Fèmen Panda a @@ -801,6 +816,7 @@ Resevwa avètisman lè anrejistreman yon konferans pare. Ilimite + Kesyon %d kesyon @@ -811,16 +827,17 @@ %s pwen %s pwen + Limit Tan %1$s Nouvo Avètisman jèm Like Antre jèm - %s like %s likes + Wouj Woz Lavand @@ -879,6 +896,7 @@ Modifye Tablo Seleksyone ki kou ou ta renmen wè nan Tablo a Modifye lis kou w la + Tablo Anvan Paj %d sou %d @@ -906,8 +924,8 @@ Tape pou afiche anons. Aksepte Refize - sou + Pa gen fichye ki asosye ak kou sa a. Pa gen fichye ki asosye ak gwoup sa a. @@ -968,6 +986,7 @@ Nou pa an mezi pou nou jwenn yon app ekstèn pou afiche zouti LTI sa a. Kòmantè Soumèt + Premye Paj Modifye Paj Deskripsyon @@ -980,10 +999,10 @@ Konferans yo pa posib sou aparèy mobil. Fichye apèsi imaj Gen yon erè ki fèt pandan esè chajman PDF sa a. - Nou byen regrèt sa! Yo pa otorize fonksyon sa a nan afichaj elèv. Pa Gen Anyen pou Wè La a Fonksyon ki pa Valide + Ajoute Elèv Antre kòd kouplaj elèv yo ba ou a. @@ -991,6 +1010,7 @@ Asosyasyon Echwe... asire w ke kòd kouplaj ou a kòrèke li pa depase limit itilizasyon an. Fini Enkonplè + Paramèt Piblikasyon Piblikasyon Nòt @@ -1021,10 +1041,10 @@ Nòt aprè piblikasyon Ranplasman nòt Nòt aktyèl - Ouvri nan Canvas Student Afichaj Elèv + Antèt chwazi: %s Kò chwazi: %s @@ -1078,7 +1098,6 @@ Ajoute yon kòmantè videyo Ajoute yon kòmantè odyo - Gen yon erè ki fèt sanzatann pandan y ap eseye anrejistre yon odyo. Gen yon erè ki fèt sanzatann pandan y ap eseye anrejistre yon videyo. @@ -1097,6 +1116,7 @@ %s. %s %s %s %s, %s + %s Minit %s Minit @@ -1117,6 +1137,7 @@ Detay Konferans Anrejistreman Konferans ap fèt + Evalyasyon kritè %s %s, %s plis enfòmasyon @@ -1143,6 +1164,7 @@ Elimine tout nan tablo Ajoute nan tablo Ajoute tout nan tablo + Kont Sal kou @@ -1151,28 +1173,50 @@ Resous Byenvini, %1$s! Sijè Pa m - Afiche Ansyen Anons Echèk chajman Sal akèy Echèk pou rafrechi Sal akèy - - Pa gen Anyen ki gen Delè pou Jodi a %1$s delè jodi a %1$s manke Bienveni! Matyè ou yo afiche la a. Pou kounye a ou pa gen matyè. - - + App la bezwen redemare + Seleksyone + Seleksyone Peryòd Klasman + Peryòd Klasman Aktyèl + Pa reyisi chaje nòt yo + Enposib pou chaje nòt yo pou peryòd notasyon an + Echèk aktyalizasyon nòt yo + Okenn nòt pou afiche + Pa Klase + Chanje peryòd klasman + Nòt pa disponib + Sal kou + + Apèsi Sal la + Lyen Enpòtan + Aplikasyon Elèv + Kontakte Anplwaye + Chwazi yon Kou + Lyen Enpòtan + Pwofesè + Asistan Pwofesè + Resous ou yo ap afiche la a. + Enposib pou chaje resous yo + Enposib pou aktyalize resous yo Ouvri yon lòt afichaj ki pi aksesib + Destinatè Sijè Seleksyone yon kou, %s + Repons elèv sa a kache paske devwa sa a anonim. Nòt Elèv (Pa sipòte) Nòt Elèv yo pa disponib sou mobil. Tip Soumisyon Pa Sipòte Soumisyon an pa kapab afiche paske Nòt Elèv yo pa disponib sou mobil. + Ou te make li konplè. diff --git a/libs/pandares/src/main/res/values-is/strings.xml b/libs/pandares/src/main/res/values-is/strings.xml index 57fc00d033..00a0d241d5 100644 --- a/libs/pandares/src/main/res/values-is/strings.xml +++ b/libs/pandares/src/main/res/values-is/strings.xml @@ -75,8 +75,8 @@ Sýna bæta við skrá valkosti Taka upp hljóð Taka upp myndband - Ritill fyrir textaskil + Skrifa… Eitthvað fór úrskeiðis við upphleðslu skila. Skila aftur. @@ -100,10 +100,10 @@ Þetta verkefni leyfir ekki netskil Þetta verkefni leyfir ekki netskil Engin netskil - Þetta verkefni er með tengil á ytra verkfæri fyrir skil. Opna verkfæri Skilatexti + Af %s stigum Af %s st @@ -112,10 +112,10 @@ %1$s/%2$s %1$s af %2$s stigum Færa inn spáða einkunn - Lágt: %s Meðal: %s Hátt: %s + Sérsniðin einkunn Það eru engin matsviðmið fyrir þetta verkefni @@ -199,7 +199,6 @@ Engin verkefni í þessum hópi Þetta verkefni er undanskilið og verður ekki með í heildarútreikningum Dæmi - Raða eftir Tími Tegund @@ -211,6 +210,7 @@ Hnappur til að raða verkefnum, raða eftir gerð Verkefnum raðað eftir tíma Verkefnum raðað eftir gerð + Punktar\u0020 Vista @@ -218,9 +218,9 @@ Svör eru bara sýnileg þeim sem hafa birt minnst eitt svar. - Þessi skrá er læst eins og er Ritill fyrir umræðusvör + Byrjar Lýkur @@ -448,6 +448,7 @@ Hætta að vera notandi Auðkenni notanda Villa kom upp við að reyna að vera notandi + Auðkenni getur ekki verið autt Farðu í próf @@ -604,6 +605,8 @@ Æfingapróf Kannanir með einkunn Kannanir + + Verkefni Einkunnir @@ -649,12 +652,12 @@ Uppsetningarstillingar Kjörstillingar reiknings PIN-númer og fingrafar - Para við áhorfanda Láttu foreldra þína skanna þennan QR-kóða úr Canvas foreldra appinu til að para við þig. Þessi kóði rennur út á sjö dögum, eða eftir eina notkun. Pörunarkóði:\u0020 Pörunarkóða villa Ekki tókst að endurheimta pörunarkóða. Þessi eiginleiki er bara studdur fyrir nemendur. + Opna Speedgrader verkefni verkefnalisti @@ -725,8 +728,8 @@ SpeedGrader Mælir - Einkunnasleði + Stofna nýjan viðburð Slökkva á pöndunum @@ -813,6 +816,7 @@ Fá tilkynningu þegar ráðstefnuupptaka er tilbúin. Ótakmarkað + Spurning %d spurning @@ -823,6 +827,7 @@ %s punktur %s punktar + Tímamörk %1$s nýjar tilkynningar líka við @@ -994,10 +999,10 @@ Ráðstefnur eru ekki enn studdar á farsíma. Forskoðun skráar mynd Villa kom upp við að sækja þetta PDF. - Því miður! Þessi eiginleiki er ekki leyfður fyrir nemendur. Hér er ekkert að sjá Óstuddur eiginleiki + Bæta við nemanda Settu inn pörunarkóða nemanda sem þú fékkst. @@ -1036,10 +1041,10 @@ Gefðu einkunn eftir birtingu Hnekking einkunnar Núverandi einkunn - Opnast í Canvas Student Nemandasýn + Valinn haus: %s Valið meginmál: %s @@ -1111,6 +1116,7 @@ %s. %s %s %s %s, %s + %s Mínúta %s mínútur @@ -1131,6 +1137,7 @@ Upplýsingar um ráðstefnur Upptökur Ráðstefna í gangi + Viðmiðunarmat %s %s, %s frekari upplýsingar @@ -1157,6 +1164,7 @@ Fjarlægja allt af stjórnborði Bæta við stjórnborð Bæta öllu við stjórnborð + Reikningur Námssalur @@ -1165,28 +1173,50 @@ Hjálparefni Velkomin/n, %1$s! Námsefnið mitt - Skoða fyrri tilkynningar Mistókst að hlaða inn Skólastofu Mistókst að endurglæða Skólastofu - - Ekkert á skilum í dag %1$s á skilum í dag %1$s vantar Velkomin(n)! Efni þitt sést hér. Þú hefur engin efni eins og er. - - + Krefst endurræsingar á forritinu + Velja + Velja einkunnatímabil + Núverandi einkunnatímabil + Ekki tókst að sækja einkunnir + Ekki tókst að sækja einkunnir fyrir einkunnatímabil + Mistókst að endurglæða einkunnir + Engar einkunnir til að sýna + Ekki metið + Breyta einkunnatímabili + Einkunnir ekki í boði + Námssalur + + Yfirlit yfir námssal + Mikilvægir tenglar + Forrit nemenda + Samskiptaupplýsingar starfsfólks + Velja námskeið + Mikilvægir tenglar + Kennari + Aðstoðarkennari + Auðlindir þínar sjást hér. + Mistókst að hlaða auðlindum + Mistókst að endurglæða auðlindir Opnið annað auðveldara útlit + Viðtakendur Efni Veldu námskeið, %s + Svör nemanda eru falin því þetta verkefni er nafnlaust. Athugasemd nemanda (Enginn stuðningur) Ekki er stuðningur við athugasemd nemanda á farsímum. Gerð skila ekki studd Ekki hægt að birta skil því ekki er stuðningur við athugasemd nemanda á farsímum. + Þú hefur merkt þetta sem lokið. diff --git a/libs/pandares/src/main/res/values-it/strings.xml b/libs/pandares/src/main/res/values-it/strings.xml index 08ecab918c..b61e257b2c 100644 --- a/libs/pandares/src/main/res/values-it/strings.xml +++ b/libs/pandares/src/main/res/values-it/strings.xml @@ -75,8 +75,8 @@ Mostra opzioni Aggiungi file Registra audio Registra video - Editor Consegna testo + Scrivi… Si è verificato un errore durante il caricamento della consegna. Invia di nuovo. @@ -100,10 +100,10 @@ Questo compito non consente consegne online Questo compito non consente consegne online Nessuna consegna online - Questo compito include link a uno strumento esterno per le consegne. Apri strumento Testo invio + Su %s punti su %s pt @@ -112,10 +112,10 @@ %1$s/%2$s %1$s su %2$s punti Inserisci punteggio What-If - Basso: %s Medio: %s Alto: %s + Punteggio personalizzato Non c’è alcuna rubrica per questo compito @@ -199,7 +199,6 @@ Nessun compito in questo gruppo Questo compito è giustificato e non verrà considerato nel calcolo totale ES - Ordina per Ora Digita @@ -211,6 +210,7 @@ Pulsante Ordina compiti, ordina per tipo Compiti ordinati per tempo Compiti ordinati per tipo + Punti\u0020 Salva @@ -219,8 +219,8 @@ Le risposte sono visibili solo a chi ha pubblicato almeno una risposta. Questo file è attualmente bloccato - Editor risposta discussione + Inizia Termina @@ -448,6 +448,7 @@ Smetti di agire come utente ID utente Si è verificato un errore durante il tentativo di agire come utente + L’ID non può essere lasciato vuoto Vai al quiz @@ -604,6 +605,8 @@ Quiz di esercitazione Sondaggi con valutazione Sondaggi + + Elenco attività Voti @@ -649,12 +652,12 @@ Impostazioni profilo Preferenze account PIN e impronta digitale - Accoppia con osservatore Il tuo elemento principale deve eseguire la scansione di questo codice QR dall’app Canvas Parent per accoppiarlo con te. Questo codice scadrà tra sette giorni o dopo un utilizzo. Accoppiamento codice:\u0020 Errore codice accoppiamento Impossibile recuperare un codice accoppiamento. Questa funzione è supportata solo per gli studenti. + Apri SpeedGrader elenco attività attività attività attività @@ -725,8 +728,8 @@ SpeedGrader Gauge - Cursore valutazione + Crea nuovo evento Disattiva i panda @@ -813,6 +816,7 @@ Ricevi notifiche quando è pronta una registrazione della conferenza. Senza limiti + Domanda %d domanda @@ -823,6 +827,7 @@ %s punto %s punti + Limite di tempo %1$s nuove notifiche mi piace @@ -994,10 +999,10 @@ Le conferenze non sono ancora supportate sul dispositivo mobile. Immagine anteprima file Si è verificato un errore durante il tentativo di caricare questo PDF. - Spiacenti! Questa funzione non è consentita nella visualizzazione studenti. Non c’è niente da vedere qui Funzione non supportata + Aggiungi studente Inserisci il codice accoppiamento studente fornito. @@ -1036,10 +1041,10 @@ Voto dopo pubblicazione Sostituzione voto Voto attuale - Si apre in Canvas Student Student View + Testa scelta: %s Corpo scelto: %s @@ -1111,6 +1116,7 @@ %s. %s %s %s %s, %s + %s minuto %s minuti @@ -1131,6 +1137,7 @@ Dettagli conferenza Registrazioni Conferenza in corso + Valutazione basata su criterio %s %s, %s maggiori informazioni @@ -1157,6 +1164,7 @@ Rimuovi tutto dalla dashboard Aggiungi alla dashboard Aggiungi tutto alla dashboard + Account Homeroom @@ -1165,28 +1173,50 @@ Risorse Benvenuto %1$s! Le mie materie - Visualizza annunci precedenti Impossibile caricare homeroom Impossibile aggiornare homeroom - - Niente in scadenza oggi %1$s in scadenza oggi %1$s mancante Benvenuto! Le materie sono visualizzate qui. Attualmente non hai materie. - - + Richiede il riavvio dell’app + Seleziona + Seleziona Periodo di valutazione + Periodo di valutazione attuale + Impossibile caricare voti + Impossibile caricare voti per il periodo di valutazione + Impossibile aggiornare i voti + Nessun voto da visualizzare + Non valutato + Cambia periodo di valutazione + Voti non disponibili + Homeroom + + Visualizzazione homeroom + Link importanti + Applicazioni studente + Informazioni di contatto del personale + Scegli un corso + Link importanti + Insegnante + Assistente del docente + Le tue risorse vengono visualizzate qui + Impossibile caricare le risorse + Impossibile aggiornare le risorsa Apri una vista alternativa più accessibile + Destinatari Oggetto Seleziona un corso, %s + Le risposte di questo studente sono nascoste perché questo compito è anonimo. Annotazione studente (non supportata) L’annotazione studente non è attualmente supportata sui dispositivi mobili. Tipo di consegna non supportato Impossibile visualizzare consegna perché l’annotazione studente non è attualmente supportata sui dispositivi mobili. + È stato contrassegnato come fatto. diff --git a/libs/pandares/src/main/res/values-ja/strings.xml b/libs/pandares/src/main/res/values-ja/strings.xml index 83f8442f63..bd1bebd5de 100644 --- a/libs/pandares/src/main/res/values-ja/strings.xml +++ b/libs/pandares/src/main/res/values-ja/strings.xml @@ -54,10 +54,8 @@ 企図が使用されました 残り試行回数なし 課題を再提出する - 外部ツールを起動中 外部ツールを起動する - プレビュー 1つ以上のファイルのアップロードに失敗しました。インターネット接続をチェックしてから、提出を再試行してください。 %1$s / %2$s @@ -68,19 +66,20 @@ 提出を削除しました 欠如 採点済み + URL を \'http://\' でプレビューすることはできません 有効なURLを入力してください 提出するにはこちらに URL を入力してください 追加ファイルオプションを表示する オーディオを録音する - 動画を録画する - テキスト提出エディタ + … を書く 提出物のアップロードで不具合が発生しました。もう一度提出します。 + 提出 提出バージョン @@ -103,6 +102,7 @@ この課題は、提出用の外部ツールにリンクします。 オープンツール テキストを提出する + /%s 点 /%s 点 @@ -114,10 +114,12 @@ 低:%s 中程度:%s 高:%s + カスタムスコア この課題に説明はありません 長い説明を表示 + %1$d / %2$d のアップロード %s へのコメントのアップロード @@ -137,6 +139,7 @@ メディアファイル オーディオ ビデオ + バージョン: @@ -186,6 +189,8 @@ %1$s の %2$s が期限 am pm + + ロックされています 課題 @@ -204,15 +209,17 @@ 課題の並べ替えボタン、タイプごとに並べ替え 時間ごとに並べ替えされた課題 タイプごとに並べ替えされた課題 + ポイント\u0020 保存 キャンセル + 回答は、少なくとも一つの回答を掲載している人にのみ表示されます。 - このファイルは現在ロックされています ディスカッション返信エディタ + 開始 終了 @@ -231,8 +238,6 @@ 仮定のスコア %1$s/%2$s (%3$s) %1$s / %2$s 点、%3$s - - 受信トレイ 未読 アーカイブ済み @@ -393,8 +398,8 @@ 受講生 オブザーバー シラバスが追加されていません。 - モジュールの読込時にエラーが発生しました。 + Canvas 受信者(複数可)を選択 このメッセージには現在、受信者はいません。 @@ -420,6 +425,7 @@ 削除されました + Canvas コンテンツ…を読み込み中 UnknownDevice @@ -436,9 +442,11 @@ ユーザーとして行動することを止める ユーザーID ユーザーとして行動しようとした際にエラーが発生しました + IDは空欄にはできません クイズへ移動 + クイズ 最後のポスト @@ -497,8 +505,8 @@ モジュールに移動 完了にする モジュールアイテムが見つかりません - モジュールの読込時にエラーが発生しました。 + ヘルプ 講師の質問 リンク @@ -591,6 +599,8 @@ 練習クイズ 採点されたアンケート サーベイ + + タスク 成績 @@ -636,12 +646,12 @@ プロファイル設定 アカウントの設定 PIN と指紋 - オブザーバーとペアにする あなたとペアリングするために、親にCanvas ParentアプリからこのQRコードをスキャンしてもらいます。このコードは7日間後または1回使用した後に有効期限が切れます。 ペアリングコード:\u0020 ペアリングコードエラー ペアリングコードを取得できません。この機能は受講生にのみサポートされています。 + SpeedGraderを開く するべきこと するべきこと するべきことリスト @@ -712,8 +722,8 @@ SpeedGrader Gauge - 成績スライダー + 新しいイベントを作成 パンダをオフにする @@ -800,6 +810,7 @@ 会議録画の準備ができたときに通知を受け取ります。 無制限 + 質問 %dの質問 @@ -808,6 +819,7 @@ %s 点 + 時間制限 %1$s 新規通知 いいね @@ -976,10 +988,10 @@ 会議はまだモバイルではサポートされていません。 ファイルプレビュー画像 このPDFを削除しようとしてエラーが発生しました。 - 申し訳ありません!この機能は受講生のビューでは許可されていません。 ここには何もありません サポートされていない機能 + 受講生を追加 提供されている受講生ペアリングコードを入力します。 @@ -1016,10 +1028,10 @@ 投稿後の成績 成績上書き 現在の成績 - Canvas受講生内で開く 受講生ビュー + 選択した頭部:%s 選択したボディ:%s @@ -1091,6 +1103,7 @@ %s. %s %s %s %s, %s + %s 分 @@ -1110,6 +1123,7 @@ 会議の詳細 録画 会議進行中 + 基準の評価 %s %s、%s 詳細はこちら @@ -1136,6 +1150,7 @@ ダッシュボードすべてから削除 ダッシュボードに追加 ダッシュ​​ボードにすべてを追加 + アカウント ホームルーム @@ -1144,28 +1159,50 @@ リソース ようこそ、%1$sさん! マイ教科 - 前の通知を表示する ホームルーム読み込みに失敗 ホームルーム更新に失敗 - - 今日が期限のものなし %1$s 今日が期限 %1$s 欠如 どういたしまして! あなたの科目はここに表示されます。 現在、科目はありません。 - - + アプリの再起動が必要 + 選択 + 採点期間を選択 + 現在の採点期間 + 成績読み込みに失敗 + 採点期間中に成績をロードできませんでした + 成績更新に失敗 + 表示する成績なし + 未採点 + 採点期間を変更 + 利用可能な成績なし + ホームルーム + + ホームルームビュー + 重要なリンク + 生徒アプリケーション + スタッフの連絡先情報 + コースを選択する + 重要なリンク + 講師 + 講師の助手 + あなたリソースはここに表示されます。 + リソース読み込みに失敗 + リソース更新に失敗 よりアクセスしやすい代替ビューを開く + 受信者 件名 コースを選択する、%s + この課題は匿名であるため、この学生の応答は表示されません。 学生の注釈(サポートされていません) 現在、学生の注釈はモバイルではサポートされていません。 サポートされていない提出タイプ 現在、学生の注釈はモバイルでサポートされていないため、提出物を表示できません。 + 「終了」としてマークしました。 diff --git a/libs/pandares/src/main/res/values-mi/strings.xml b/libs/pandares/src/main/res/values-mi/strings.xml index 7ad657b5c5..3a4412e013 100644 --- a/libs/pandares/src/main/res/values-mi/strings.xml +++ b/libs/pandares/src/main/res/values-mi/strings.xml @@ -1176,24 +1176,47 @@ Titiro ngā Pānuitanga o muri I rahua te uta i te Kaingarūma I rahua ki te whakahou te Kaingaruma - Kaore e Tika ana mō tēnei rā %1$s e tika ana mo tēnei rā %1$s e ngaro ana Nau Mai! Ka kitea ō marau i konei. Kaore koe e whiwhi marau i tēnei wā. - - + E hiahia taupanga timata + Tīpakohia + Tīpakohia te Kōeke Wā + Hanga Kōeke Wā + I rāhua te uta i ngā kōeke + I rāhua te uta i ngā kōeke mo te wā o te kōeke + I rāhua te whakahou i ngā kōeke + Kaore he kōeke hei whakaatu + Kāore i kōekehia + Huri ana i ngā wā kōeke + Kāore ngā kōeke i te wātea + Kainga rūma + + Kainga rūma Tiro + Ngā Hono Mea Nui + Ākonga Tono + Kaimahi Whakapā Pūrongo + Kōwhiri he Akoranga + Ngā Hono Mea Nui + Kaiako + Kaiako Kaiāwhina + Ka kitea ō rauemi i konei. + I rahua te uta i ngā rauemi + I rāhua te whakahou i ngā rauemi Huaki he wāhanga whakauru ngāwari mo tētahi tiro atu Ngā Kaiwhiwhi Kaupapa Tīpako he akoranga, %s + Kei te hunahia ngā whakautu a tēnei ākonga no te mea he ingoamuna tēnei whakataunga. Ākonga Tuhipoka (Kaore e tautokotia) Ākonga Tuhipoka kaore i te tautokotia i tēnei wā i runga i te waea haerere. Kaore i Tautokotia tēnei Momo Tapaetanga Kaore e taea te whakaatu atu i te tāpaetanga no te mea Ākonga Tuhipoka kaore i te tautokotia i tēnei wā i runga i te waea haerere. + Kua tohua e koe kua oti. diff --git a/libs/pandares/src/main/res/values-nb/strings.xml b/libs/pandares/src/main/res/values-nb/strings.xml index 70215f1cd2..8a4a5831a1 100644 --- a/libs/pandares/src/main/res/values-nb/strings.xml +++ b/libs/pandares/src/main/res/values-nb/strings.xml @@ -55,10 +55,8 @@ Forsøk brukt Ingen forsøk igjen Send oppgaven inn på nytt - Starter eksternt verktøy… Åpne eksternt verktøy - Forhåndsvisning En eller flere filer kunne ikke lastes opp. Sjekk internettforbindelsen din og prøv å lever på nytt. %1$s av %2$s @@ -69,19 +67,20 @@ Innlevering slettet Mangler Karaktersatt + Det er ingen forhåndsvisnings-URL ved bruk av \'http://\' Skriv inn gyldig URL Skriv inn en URL her for innleveringen din Vis Legg til fil alternativer Lydopptak - Ta opp video - Redigeringsprogram for tekstinnlevering + Skriv… Noe gikk galt ved opplasting av innlevering. Lever på nytt. + Innlevering Innlevering versjoner @@ -104,6 +103,7 @@ Denne oppgaven er lenket til et eksternt verktøy for innleveringer. Åpne verktøy Innleveringstekst + Av %s poeng Av %s poeng @@ -115,10 +115,12 @@ Lav: %s Gjennomsnitt: %s Høy: %s + Egendefinert resultat Det er ingen vurderingsveiledning for denne oppgaven Vis lang beskrivelse + Laster opp fil %1$d av %2$d Last opp kommentar for %s @@ -138,6 +140,7 @@ Mediefil Lyd Video + Versjon: @@ -187,6 +190,8 @@ Frist %1$s klokken %2$s am pm + + Låst Oppgaver @@ -205,15 +210,17 @@ Sorter oppgaver-knapp, sorter etter type Oppgaver sortert etter tid Oppgaver sortert etter type + Poeng\u0020 Lagre Avbryt + Svarene er kun synlige for dem som har postet minst ett svar. - Denne filen er for tiden låst Redigeringsprogram for diskusjonssvar + Starter Slutter @@ -232,8 +239,6 @@ Hva-om poengsum %1$s/%2$s (%3$s) %1$s av %2$s poeng, %3$s - - Innboks Ulest Arkivert @@ -399,8 +404,8 @@ Studenter Observatører Et sammendrag har ikke blitt lagt til. - Det var et avvik ved lasting av modulene dine. + Canvas Velg mottaker(e) Denne meldingen har foreløpig ingen mottakere. @@ -426,6 +431,7 @@ Slettet + Laster Canvas-innhold… UnknownDevice @@ -442,9 +448,11 @@ Slutt å opptre som bruker Bruker-ID Det oppsto en feil da du prøvde å opptre som bruker + ID kan ikke være tom Gå til quiz + Tester Siste innlegg @@ -503,8 +511,8 @@ Gå til moduler Merk ferdig Modulen ble ikke funnet - Det var et avvik ved lasting av modulene dine. + Hjelp Instruktør-spørsmål Lenke @@ -520,11 +528,11 @@ Innlegging av tekst URL på nett Innleveringen tillater kun én filopplasting - - Legg inn nettside-innlegg Medieinnspillinger Send inn + + Ulagret fremdrift All ulagret informasjon vil bli tapt. Ønsker du å fortsette? @@ -539,6 +547,7 @@ Neste Legg til en kommentar… + Hjem Varslinger @@ -568,6 +577,8 @@ Det ble tatt et øyeblikksbilde av nettstedet når du leverte det inn. Tapp og hold på bilde nedenfor for å åpne eller laste ned hele bildet. Denne innleveringen var en URL til en ekstern side. Husk at denne siden kan ha endret seg siden innleveringen opprinnelig skjedde. Forhåndsvisning av den vedlagte url + + %1$s støttes ikke. Lenken støttes ikke. Åpne i nettleser @@ -583,15 +594,19 @@ Panda-fakta:  Fjern Grunnleggere + EULA Personvernregler Bruksvilkår Canvas på GitHub + Oppgavetester Øvelsestester Karaktergivende spørreundersøkelser Spørreundersøkelser + + Å gjøre Karakterer @@ -637,12 +652,12 @@ Profilinnstillinger Kontoinnstillinger PIN og fingeravtrykk - Par med observatør Få en forelder til å skanne denne QR-koden fra Canvas foreldreapplikasjonen for å pare med deg. Denne koden vil utløpe om sju dager, eller etter en gangs bruk. Paringskode:\u0020 Feil med paringskode Kunne ikke hente en paringskode. Denne funksjonen støttes kun for studenter. + Åpne Speedgrader todo to do todos todo list @@ -713,8 +728,8 @@ SpeedGrader Gauge - Karakterglidebryter + Opprett ny hendelse Skru av pandaene @@ -801,6 +816,7 @@ Bli varslet når et konferanseopptak er klart. Ubegrenset + Spørsmål %d spørsmål @@ -811,16 +827,17 @@ %s poeng %s poeng + Tidsbegrensning %1$s Nye varsler liker Angi likerklikk liker - %s liker %s likerklikk + Rød Varmrosa Lavendel @@ -879,6 +896,7 @@ Rediger dashbord Velg hvilke emner du vil se på dashbordet Rediger emnelisten + Instrumentbord Forrige Side %d av %d @@ -906,8 +924,8 @@ Trykk for å vise kunngjøring Godta Avslå - ut av + Det finnes ingen filer assosiert med dette kurset. Det finnes ingen filer assosiert med denne gruppa. @@ -968,6 +986,7 @@ Vi finner ikke et eksternt program for å vise dette LTI-verktøyet. Last opp kommentar + Forside Endre side Beskrivelse @@ -980,10 +999,10 @@ Konferanser støttes ikke på mobil. Forhåndsvisning av filen En feil oppsto under innlastingen av denne PDF-en. - Beklager! Denne funksjonen er ikke tillatt i studentvisning. Ingenting å se her Ikke støttet funksjon + Legg til student Oppgi studentparingskoden som ble gitt til deg. @@ -991,6 +1010,7 @@ Paring mislyktes. Pass på at paringskoden er korrekt og innenfor tidsbegrensningen for bruk. Godkjent Ikke godkjent + Publiseringsinnstillinger Publisert karakterer @@ -1021,10 +1041,10 @@ Karakter etter publisering Overskriv karakter Gjeldende karakter - Åpnes i Canvas Student Studentvisning + Valgt hode: %s Valgt kropp: %s @@ -1078,7 +1098,6 @@ Legg til en videokommentar Legg til en lydkommentar - En uventet feil oppsto ved lydopptak. En uventet feil oppsto ved videoopptak. @@ -1097,6 +1116,7 @@ %s. %s %s %s %s, %s + %s minutt %s minutter @@ -1117,6 +1137,7 @@ Konferanse informasjon Opptak Konferanse pågår + Kriterievurdering %s %s, %s mer informasjon @@ -1143,6 +1164,7 @@ Fjern alle fra dashbord Legg til dashbord Legg til alle til dashbord + Konto Hjem @@ -1151,28 +1173,50 @@ Ressurser Velkommen, %1$s! Mine emner - Vis tidligere kunngjøringer Kunne ikke laste inn Homeroom Kunne ikke oppdatere Homeroom - - Ingenting forfaller i dag %1$s forfaller i dag %1$s mangler Velkommen! Emnene dine vises her. Du har for øyeblikket ingen emner. - - + Krever omstart av appen + Velg + Velg vurderingsperiode + Gjeldende vurderingsperiode + Kunne ikke laste karakterer + Kunne ikke laste inn karakterer for vurderingsperioden + Kunne ikke oppdatere karakterer + Ingen karakterer å vise + Ikke karaktersatt + Endre vurderingsperiode + Karakterer er ikke tilgjengelig + Hjem + + Vis hjemmerom + Viktige lenker + Studentsøknader + Kontaktinformasjon personale + Velg et emne + Viktige lenker + Lærer + Lærerassistent + Ressusrene dine vises her. + Kunne ikke laste opp ressurser + Kunne ikke oppdatere ressurser Åpne en mer tilgjengelig alternativ visning + Mottakere Emnetittel Velg et emne, %s + Denne studentens svar er skjult fordi denne oppgaven er anonym. Studentmerknad (ikke støttet) Studentmerknader støttes for øyeblikket ikke på mobil. Ustøttet innleveringstype Innlevering kan ikke vises fordi Studentmerknader for øyeblikket ikke støttes på mobil. + Du har merket det som fullført. diff --git a/libs/pandares/src/main/res/values-nl/strings.xml b/libs/pandares/src/main/res/values-nl/strings.xml index 2a4f2e6ab5..f666c72ca7 100644 --- a/libs/pandares/src/main/res/values-nl/strings.xml +++ b/libs/pandares/src/main/res/values-nl/strings.xml @@ -55,10 +55,8 @@ Gebruikte pogingen Geen resterende pogingen Opdracht opnieuw inleveren - Externe tool starten... Extern hulpmiddel lanceren - Voorbeeld Er kunnen een of meer bestanden niet worden geüpload. Controleer je internetverbinding en probeer opnieuw in te leveren. %1$s van %2$s @@ -69,19 +67,20 @@ Inlevering verwijderd Ontbrekend Beoordeeld + Geen voorbeeld beschikbaar voor URL\'s die \'http://\' gebruiken Voer een geldige URL in Voer hier een URL in voor uw inzending Opties weergeven voor bestand toevoegen Audio opnemen - Video opnemen - Editor voor tekstinlevering + Schrijven… Er is iets mis gegaan bij het uploaden van de opdracht. Opnieuw inleveren. + Inzending Inzendingsversies @@ -104,6 +103,7 @@ Deze opdracht heeft een link naar een externe tool voor inleveringen. Tool openen Inleveringstekst + Zonder %s punten Zonder %s punten @@ -115,10 +115,12 @@ Laag: %s Gemiddelde: %s Hoog: %s + Aangepaste score Er is geen rubriek voor deze opdracht Uitgebreide beschrijving bekijken + Bestand %1$d van %2$d wordt geüpload Opmerking uploaden voor %s @@ -138,6 +140,7 @@ Mediabestand Audio Video + Versie: @@ -187,6 +190,8 @@ In te leveren op %1$s om %2$s s ochtends s middags + + Vergrendeld Opdrachten @@ -205,15 +210,17 @@ Knop voor opdrachten sorteren, sorteren op type Opdrachten gesorteerd op tijd Opdrachten gesorteerd op type + Punten\u0020 Opslaan Annuleren + Antwoorden zijn alleen zichtbaar voor degenen die minimaal één antwoord hebben geplaatst. - Dit bestand is momenteel vergrendeld Editor voor discussieantwoorden + Begint op Eindigt @@ -232,8 +239,6 @@ What-If score %1$s/%2$s (%3$s) %1$s van de %2$s punten, %3$s - - Inbox Ongelezen Gearchiveerd @@ -399,8 +404,8 @@ Cursisten Waarnemers Er is geen syllabus toegevoegd. - Er is een fout opgetreden bij het laden van uw modules. + Canvas Ontvanger(s) kiezen Dit bericht heeft momenteel geen ontvangers. @@ -426,6 +431,7 @@ Verwijderd + Canvas Content aan het uploaden… UnknownDevice @@ -442,9 +448,11 @@ Stoppen met optreden als gebruiker Gebruikers-ID: Er heeft zich een fout voorgedaan bij optreden als gebruiker + Het ID mag niet leeg zijn Ga naar toets + Toetsen Laatste bericht @@ -503,8 +511,8 @@ Ga naar modulen Gereed markeren Module-item niet gevonden - Er is een fout opgetreden bij het laden van uw modules. + Help Instructeurvraag Link @@ -520,11 +528,11 @@ Tekstinvoer Online URL Je kunt niet meer dan één bestand uploaden - - Ingang voor website toevoegen Media opnames Inleveren + + Onbeveiligde voortgang Niet-opgeslagen informatie gaat verloren. Wil je doorgaan? @@ -539,6 +547,7 @@ Volgende Een opmerking toevoegen… + Startpagina Meldingen @@ -568,6 +577,8 @@ Er is een snapshot van de website gemaakt toen je het inleverde. Tik en houd het onderstaande beeld vast om het volledige beeld te openen of downloaden. Deze inzending was een URL naar een externe pagina. Houd in gedachten dat deze pagina gewijzigd kan zijn na de oorspronkelijke inzending. Een voorbeeld van de ingediende url + + %1$s worden niet ondersteund. De link wordt niet ondersteund. Openen in browser @@ -583,15 +594,19 @@ Panda feit:  Verwijderen Stichters + EULA Privacybeleid Gebruiksvoorwaarden Canvas op GitHub + Toetsen met opdrachten Practicumtoetsen Beoordeelde onderzoeken Enquêtes + + To-do lijst Cijfers @@ -637,12 +652,12 @@ Profielinstellingen Accountvoorkeuren PIN en vingerafdruk - Koppelen met waarnemer Laat je ouder deze QR-code scannen van de Canvas Parent-app om met jou te koppelen. Deze code vervalt over zeven dagen of na één keer gebruiken. Koppelingscode:\u0020 Koppelingscodefout Kan koppelingscode niet ophalen. Deze functie wordt alleen ondersteund voor cursisten. + Speedgrader openen to do to do to do to do lijst @@ -713,8 +728,8 @@ SpeedGrader Gauge - Cijferslider + Een nieuwe gebeurtenis maken Panda\'s uitzetten @@ -801,6 +816,7 @@ Ontvang een melding wanneer er een opname van een vergadering klaar is. Onbeperkt + Vraag %d vraag @@ -811,16 +827,17 @@ %s punt %s punten + Tijdslimiet %1$s Nieuwe meldingen like Invoeren van likes likes - %s like %s likes + Rood Felroze Lavendel @@ -879,6 +896,7 @@ Dashboard bewerken Selecteer de cursussen die je in het Dashboard wilt weergeven Bewerk je lijst met cursussen + Dashboard Vorige Pagina %d van %d @@ -906,8 +924,8 @@ Tik om aankondiging weer te geven Accepteren Weigeren - van + Er zijn geen bestanden gekoppeld aan deze cursus. Er zijn geen bestanden gekoppeld aan deze groep. @@ -968,6 +986,7 @@ We kunnen geen externe app vinden om deze LTI-tool weer te geven. Opmerking uploaden + Voorpagina Pagina bewerken Beschrijving @@ -980,10 +999,10 @@ Vergaderingen worden nog niet ondersteund op mobiele apparatenó. Voorbeeldweergave bestand Er is een fout opgetreden bij het laden van deze PDF. - Sorry! Deze functie is niet toegestaan in de cursistweergave. Niets te zien hier Niet ondersteunde functie + Cursist toevoegen Voer de koppelingscode cursist in die je gekregen hebt. @@ -991,6 +1010,7 @@ Koppelen mislukt. Zorg ervoor dat je koppelingscode juist is en dat deze binnen de ingestelde tijdslimiet valt. Voltooid Incompleet + Instellingen voor posten Cijfers posten @@ -1021,10 +1041,10 @@ Beoordelen na posten Cijferoverschrijving Huidig cijfer - Opent in Canvas-cursist Weergave voor cursisten + Gekozen kop: %s Gekozen hoofdtekst: %s @@ -1078,7 +1098,6 @@ Videocommentaar toevoegen Audiocommentaar toevoegen - Er heeft zich een onverwachte fout voorgedaan bij het opnemen van audio. Er heeft zich een onverwachte fout voorgedaan bij het opnemen van video. @@ -1097,6 +1116,7 @@ %s. %s %s %s %s, %s + %s minuut %s minuten @@ -1117,6 +1137,7 @@ Vergaderingdetails Opnamen Vergadering is bezig + Criteriabeoordeling %s %s, %s meer informatie @@ -1143,6 +1164,7 @@ Alles verwijderen uit Dashboard Toevoegen aan Dashboard Alles toevoegen aan Dashboard + Account Klas @@ -1151,28 +1173,50 @@ Hulpbronnen Welkom, %1$s! Mijn onderwerpen - Vorige aankondigingen bekijken Laden van klas mislukt Verversen van klas mislukt - - Vandaag niets in te leveren %1$s vandaag in te leveren %1$s ontbreekt Welkom! Je vakken worden hier getoond. Je hebt momenteel geen vakken. - - + Vereist herstarten van app + Selecteren + Beoordelingsperiode selecteren + Huidige beoordelingsperiode + Laden van cijfers mislukt + Kan cijfers voor beoordelingsperiode niet laden + Verversen van cijfers mislukt + Geen cijfers om weer te geven + Niet beoordeeld + Beoordelingsperiode wijzigen + Cijfers niet beschikbaar + Klas + + Huisklasweergave + Belangrijke links + Cursistapplicaties + Contactgegevens van medewerkers + Een cursus kiezen + Belangrijke links + Docent + Onderwijsassistent + Je bronnen worden hier getoond. + Laden van bronnen mislukt + Verversen van bronnen mislukt Open een meer toegankelijke alternatieve weergave + Geadresseerden Vak Selecteer een cursus, %s + De antwoorden van deze cursist zijn verborgen omdat deze opdracht anoniem is. Cursistaantekening (niet ondersteund) Cursistaantekening wordt momenteel niet ondersteund op mobiel apparaat. Niet-ondersteund inleveringstype Inlevering kan niet worden weergegeven omdat cursistaantekening momenteel niet wordt ondersteund op mobiel apparaat. + Je hebt het als klaar gemarkeerd. diff --git a/libs/pandares/src/main/res/values-pl/strings.xml b/libs/pandares/src/main/res/values-pl/strings.xml index cb5857dd20..a0874855a2 100644 --- a/libs/pandares/src/main/res/values-pl/strings.xml +++ b/libs/pandares/src/main/res/values-pl/strings.xml @@ -77,8 +77,8 @@ Pokaż opcje dodawania plików Nagraj dźwięk Nagraj dźwięk - Edytor zgłoszeń tekstowych + Napisz… Wystąpił problem z przesyłaniem. Prześlij ponownie. @@ -102,10 +102,10 @@ To zadanie nie umożliwia przesyłania online To zadanie nie umożliwia przesyłania online Brak przesyłek online - To zadanie odnosi się do zewnętrznego narzędzia do przesyłania. Otwórz narzędzie Tekst przesyłki + Z %s pkt Z %s pkt @@ -114,10 +114,10 @@ %1$s/%2$s %1$s z %2$s pkt Wprowadź wynik dla a-co-jeśli - Niski: %s Średni: %s Wysoki: %s + Niestandardowy wynik punktowy Nie ma rubryki dla tego zadania @@ -201,7 +201,6 @@ Brak zadań w danej grupie To zadanie zostało usprawiedliwione i nie zostanie ujęte w ostatecznym rozliczeniu EX - Sortuj wg Czas Typ @@ -213,6 +212,7 @@ Przycisk sortowania zadań, sortuj wg typu Zadania posortowane wg czasu Zadania posortowane wg typu + Punkty\u0020 Zapisz @@ -220,9 +220,9 @@ Odpowiedzi są widoczne tylko dla tych osób, które opublikowały co najmniej jedną odpowiedź. - Aktualnie plik jest zablokowany Edytor odpowiedzi w dyskusji + Rozpoczyna się Kończy się @@ -460,6 +460,7 @@ Przestań działać jako użytkownik ID użytkownika Wystąpił błąd podczas działania jako użytkownik + Pole identyfikatora nie może być puste Przejdź do testu @@ -616,6 +617,8 @@ Testy próbne Ocenione ankiety Ankiety + + Lista zadań Oceny @@ -661,12 +664,12 @@ Ustawienia profilu Preferencje konta Kod PIN i linie papilarne - Paruj z obserwującym Poproś rodzica o zeskanowanie tego kodu QR w aplikacji Canvas Parent w celu sparowania. Ten kod wygaśnie za siedem dni lub po jednym jego wykorzystaniu. Kod parowania:\u0020 Błąd kodu parowania Nie udało się pobrać kodu parowania. Funkcji tej mogą używać tylko uczestnicy. + Otwórz Speedgrader do zrobienia do zrobienia lista do zrobienia @@ -737,8 +740,8 @@ SpeedGrader Gauge - Suwak stopni + Utwórz nowe wydarzenie Wyłącz pandy @@ -825,6 +828,7 @@ Powiadamiaj mnie o gotowości do nagrywania konferencji. Nieograniczony + Pytanie %d pytanie @@ -839,6 +843,7 @@ %s punkty %s punkty + Limit czasu Nowe powiadomienia: %1$s lubię to @@ -1016,10 +1021,10 @@ Konferencje nie mają jeszcze wsparcia dla urządzeń przenośnych. Obraz podglądu pliku Podczas wczytywania tego pliku PDF wystąpił błąd. - Przepraszamy! Funkcja ta nie jest dozwolona w widoku uczestnika. Nic tu nie ma Nieobsługiwana funkcja + Dodaj ucznia Wpisz otrzymany kod parowania uczestnika. @@ -1062,10 +1067,10 @@ Ocena po opublikowaniu Nadpisująca ocena Bieżąca ocena - Otwiera się w Canvas Student Widok uczestnika + Wybrana głowa: %s Wybrany tułów: %s @@ -1137,6 +1142,7 @@ %s. %s %s %s %s, %s + %s min %s min @@ -1159,6 +1165,7 @@ Szczegóły konferencji Nagrań Konferencja w toku + Punktacja dla danego kryterium %s %s, %s więcej informacji @@ -1185,6 +1192,7 @@ Usuń wszystko z panelu nawigacyjnego Dodaj do panelu nawigacyjnego Dodaj wszystko do panelu nawigacyjnego + Konto Pokój główny @@ -1193,28 +1201,50 @@ Zasoby Witamy, %1$s! Moje tematy - Wyświetl poprzednie ogłoszenia Nie udało się załadować pokoju głównego Nie udało się odświeżyć pokoju głównego - - Brak elementów z dzisiejszym terminem %1$s z terminem dzisiaj Brak %1$s Witamy! Twoje tematy pojawią się tutaj. Nie masz obecnie żadnych tematów. - - + Wymaga ponownego uruchomienia aplikacji + Wybierz + Wybierz okres oceniania + Bieżący okres oceniania + Nie udało się załadować ocen + Nie udało się wczytać ocen dla okresu oceniania + Nie udało się odświeżyć ocen + Brak ocen do wyświetlenia + Nie oceniono + Zmień okres oceniania + Brak dostępnych ocen + Pokój główny + + Widok pokoju głównego + Ważne łącza + Aplikacje uczestników + Dane kontaktowe pracowników + Wybierz kurs + Ważne łącza + Nauczyciel + Asystent instruktora + Twoje zasoby pojawią się tutaj. + Nie udało się załadować zasobów + Nie udało się odświeżyć zasobów Otwórz alternatywny widok z ułatwieniami dostępu + Odbiorcy Temat Wybierz kurs, %s + Odpowiedzi tego uczestnika są ukryte, ponieważ to zadanie jest anonimowe. Adnotacja uczestnika (nieobsługiwana) Adnotacja uczestnika nie jest obecnie obsługiwana na urządzeniach przenośnych. Nieobsługiwany typ zgłoszenia Przesyłki nie można wyświetlić, ponieważ Adnotacja uczestnika nie jest obecnie obsługiwana na urządzeniach przenośnych. + Oznaczono je jako gotowe. diff --git a/libs/pandares/src/main/res/values-pt-rBR/strings.xml b/libs/pandares/src/main/res/values-pt-rBR/strings.xml index 602a525053..36a8ab08f0 100644 --- a/libs/pandares/src/main/res/values-pt-rBR/strings.xml +++ b/libs/pandares/src/main/res/values-pt-rBR/strings.xml @@ -75,8 +75,8 @@ Exibir Adicionar opções de arquivo Gravar Áudio Gravar Vídeo - Editor de envio de texto + Gravar… Algo deu errado com o carregamento do envio. Envie novamente. @@ -100,10 +100,10 @@ Esta tarefa não permite envios on-line Esta tarefa não permite envios on-line Nenhum envio on-line - Esta tarefa é vinculada a uma ferramenta interna para envios. Abrir ferramenta Texto de submissão + De %s pontos De %s pts @@ -111,11 +111,11 @@ Nota final: %s %1$s/%2$s %1$s de %2$s pontos - Inserir pontuação E-Se Baixa: %s Média: %s Alta: %s + Pontuação personalizada Não há nenhuma rubrica para essa tarefa @@ -199,7 +199,6 @@ Sem tarefas neste grupo Esta tarefa está dispensada e não fará parte do cálculo total EX - Ordenar por Tempo Tipo @@ -211,6 +210,7 @@ Botão Classificar tarefas, classificar por tipo Tarefas classificadas por tempo Tarefas classificadas por tipo + Pontos\u0020 Salvar @@ -218,9 +218,9 @@ As respostas são visíveis apenas àqueles que postaram pelo menos uma resposta. - Este arquivo está travado no momento Editor de resposta da discussão + Começa Termina @@ -448,6 +448,7 @@ Parar de agir como usuário ID de Usuário Ocorreu um erro ao tentar agir como usuário + O ID não pode ser branco Ir para teste @@ -598,11 +599,14 @@ Política de privacidade Termos de Uso Canvas no GitHub + Testes da tarefa Testes de prática Pesquisas avaliadas Pesquisas + + Lista de Tarefas Notas @@ -648,12 +652,12 @@ Configurações do perfil Preferências da conta PIN e Impressão digital - Emparelhar com observador Faça o seu pai escanear este código QR a partir do app Canvas Parent para emparelhar com você. Este código expirará em sete dias ou após um uso. Código de emparelhamento:\u0020 Erro do código de emparelhamento Incapaz de recuperar um código de emparelhamento. Este recurso é suportado apenas para alunos. + Abrir Speedgrader afazer a fazer afazeres lista de afazeres @@ -724,8 +728,8 @@ SpeedGrader Gauge - Controle deslizante de nota + Criar novo evento Desligar os pandas @@ -812,6 +816,7 @@ Ser notificado quando uma gravação de conferência estiver pronta. Sem limite + Pergunta %d pergunta @@ -822,16 +827,17 @@ %s ponto %s pontos + Limite de tempo %1$s Novas notificações curtir Entrada de curtidas curte - %s curtida %s curtidas + Vermelho Rosa quente Lavanda @@ -993,10 +999,10 @@ Conferências ainda não são suportadas em dispositivos móveis. Imagem de pré-visualização do arquivo Ocorreu um erro ao tentar carregar este PDF. - Desculpe-nos! Este recurso não é permitido na visualização do aluno. Nada para ver aqui Recurso não suportado + Adicionar Estudante Insira o código de emparelhamento do aluno fornecido. @@ -1035,10 +1041,10 @@ Nota após postar Substituição da nota Nota atual - Abrir no Canvas Student Visualização do aluno + Cabeça escolhida: %s Corpo escolhido: %s @@ -1131,6 +1137,7 @@ Detalhes da conferência Gravações Conferência em andamento + Avaliação de critério %s %s, %s mais informações @@ -1157,6 +1164,7 @@ Remover todos do painel Adicionar ao painel Adicionar todos ao painel + Conta Sala de aula @@ -1165,28 +1173,50 @@ Recursos Bem-vindo, %1$s! Minhas disciplinas - Exibir anúncios anteriores Falha em carregar o Homeroom Falha em atualizar o Homeroom - - Nada com prazo vencendo hoje %1$s vence hoje %1$s ausente Bem-vindo(a)! Suas disciplinas aparecem aqui. Você atualmente não tem disciplinas. - - + Precisa reiniciar o aplicativo + Selecionar + Selecionar período de avaliação + Período de pontuação atual + Falha em carregar notas + Falha ao carregar notas para o período de avaliação + Falha ao atualizar notas + Sem notas para mostrar + Sem nota + Alterar período de classificação + Notas não disponíveis + Sala de aula + + Visualização da sala de aula + Links importantes + Inscrições de alunos + Informações de contato da equipe + Escolher um curso + Links importantes + Professor + Professor assistente + Seus recursos aparecem aqui. + Falha ao carregar recursos + Falha ao atualizar recursos Abra uma exibição alternativa mais acessível + Recipientes Assunto Selecionar um curso, %s + As respostas deste aluno estão ocultas porque esta tarefa é anônima. Anotação do aluno (não compatível) Atualmente, a anotação do aluno não é compatível com dispositivos móveis. Tipo de envio não suportado O envio não pode ser exibido, porque a Anotação do Aluno não é compatível com dispositivos móveis. + Você marcou como concluído. diff --git a/libs/pandares/src/main/res/values-pt-rPT/strings.xml b/libs/pandares/src/main/res/values-pt-rPT/strings.xml index 12666bd656..4374fab19e 100644 --- a/libs/pandares/src/main/res/values-pt-rPT/strings.xml +++ b/libs/pandares/src/main/res/values-pt-rPT/strings.xml @@ -55,10 +55,8 @@ Tentativas utilizadas Não há mais tentativas Voltar a submeter tarefa - Lançar Ferramenta Externa... Lançar Ferramenta Externa - Visualização Um ou mais arquivos não foram carregados. Verifique sua conexão com a Internet e tente enviar novamente. %1$s de %2$s @@ -69,19 +67,20 @@ Submissão eliminada Em falta Classificado + Nenhuma visualização disponível para URLs usando \'http://\' Por favor, insira um URL válido Insira um URL aqui para o seu envio Mostrar opções de adicionar ficheiro Gravar Áudio - Gravar Vídeo - Editor de Submissão de Texto + Escrever… Algo deu errado no carregamento do envio. Envie novamente. + Submissão Versões de envio @@ -104,6 +103,7 @@ Esta atribuição liga a uma ferramenta externa para submissões. Abrir ferramenta Texto de submissão + Fora de %s pontos Fora de %s pts @@ -115,10 +115,12 @@ Baixa: %s Média: %s Alta: %s + Personalizar pontuação Não existe rubrica para esta atribuição Ver descrição longa + A carregar ficheiro %1$d de %2$d A carregar comentário para %s @@ -138,6 +140,7 @@ Ficheiro de mídia Áudio Vídeo + Versão: @@ -187,6 +190,8 @@ Termina %1$s a %2$s am pm + + Bloqueado Tarefas @@ -205,15 +210,17 @@ Classificar botão de tarefas, classificar por tipo Tarefas classificadas por tempo Tarefas classificadas por tipo + Pontos\u0020 Guardar Cancelar + As respostas apenas estão visíveis para aqueles que publicaram pelo menos uma resposta. - Este ficheiro está bloqueado de momento Editor da Resposta de Discussão + Inicia Termina @@ -232,8 +239,6 @@ Pontuação Potencial %1$s/%2$s (%3$s) %1$s de %2$s pontos, %3$s - - Caixa de entrada Não lida Arquivada @@ -399,8 +404,8 @@ Alunos Observadores Um plano de estudos não foi adicionado. - Houve um erro ao carregar seus módulos. + Canvas Escolher Destinatário(s) Esta mensagem atualmente não tem destinatários. @@ -426,6 +431,7 @@ Eliminado + A carregar o conteúdo da tela… UnknownDevice @@ -442,9 +448,11 @@ Parar de agir como utilizador ID do utilizador Houve um erro ao tentar agir como utilizador + A ID não pode estar vazia Ir Para Questionário + Testes Última Publicação @@ -503,8 +511,8 @@ Ir Para Módulos Marcar terminado Item do Módulo não encontrado - Houve um erro ao carregar seus módulos. + Ajuda Questões do Formador Ligação @@ -520,11 +528,11 @@ Entrada de texto URL On-line Este envio apenas aceita o carregamento de um ficheiro - - Adicionar entrada de site Gravações de multimédia Submeter + + Progresso não guardado As informações não guardadas serão perdidas. Deseja continuar? @@ -539,6 +547,7 @@ Próximo Adicionar um comentário… + Início Notificações @@ -568,6 +577,8 @@ Foi tirada uma imagem deste site quando fez a entrega. Insira e mantenha a imagem abaixo para abrir ou descarregar a imagem completa. Este envio era um URL de uma página externa. Tenha em mente que esta página pode ter sido alterada desde o envio original. Uma previsualização do url entregue + + %1$s não são suportados. A ligação não é suportada. Abrir no Navegador @@ -583,15 +594,19 @@ Fat. Panda:  Eliminar Fundadores + EULA Política de Privacidade Termos de uso Canvas no GitHub + Testes da tarefa Testes práticos Inquéritos Classificados Inquéritos + + A Fazer Classificações @@ -637,12 +652,12 @@ Configurações de perfil Preferências da conta PIN e impressão digital - Par com o observador Peça ao seu pai para digitalizar este código QR da aplicação Canvas Parent para parelhar com você. Este código expirará em sete dias, ou após uma utilização. Código de pareamento:\u0020 Erro de Código de pareamento Incapaz de recuperar um código de pareamento. Esta funcionalidade só é suportada por estudantes. + Abrir Speedgrader afazer a fazer afazer lista a fazer @@ -713,8 +728,8 @@ SpeedGrader Gauge - Seletor de classificação + Criar Novo Evento Desligar os pandas @@ -801,6 +816,7 @@ Receba notificações quando uma gravação de conferência estiver pronta. Ilimitado + Pergunta %d pergunta @@ -811,16 +827,17 @@ %s ponto %s pontos + Limite de tempo %1$s Novas Notificações gostar Entrada gostos gostos - %s gosto %s gostos + Encarnado Rosa Lavanda @@ -879,6 +896,7 @@ Editar painel Selecionar quais disciplinas você gostaria de ver no Painel Editar sua lista de disciplinas + Painel de controlo Anterior Página %d de %d @@ -906,8 +924,8 @@ Toque para ver o anúncio. Aceitar Recusar - de + Não há ficheiros associados a esta disciplina. Não há ficheiros associados a este grupo. @@ -968,6 +986,7 @@ Não conseguimos encontrar uma aplicação externa para visualizar essa ferramenta LTI. Carregamento de comentários + Primeira página Editar Página Descrição @@ -980,10 +999,10 @@ Conferências ainda não são suportadas no telemóvel. Imagem de pré-visualização de ficheiro Ocorreu um erro ao tentar carregar este PDF. - Lamento imenso! Esta funcionalidade não é permitida na visão dos alunos. Nada Para Ver Aqui Característica não suportada + Adicionar aluno Insira o código de pareamento de alunos fornecido a você. @@ -991,6 +1010,7 @@ O emparelhamento falhou. Certifique-se de que seu código de pareamento esteja correto e dentro do limite de tempo de uso. Completo Incompleto + Publicar configurações Publicar Classificações @@ -1021,10 +1041,10 @@ Classificação após publicação Substituir classificação Classificação Atual - Abrir no Aluno Canvas Vista de aluno + Cabeça escolhida: %s Corpo escolhido: %s @@ -1078,7 +1098,6 @@ Adicionar comentário de vídeo Adicionar comentário de vídeo - Ocorreu um erro inesperado ao tentar gravar áudio. Ocorreu um erro inesperado ao tentar gravar vídeo. @@ -1097,6 +1116,7 @@ %s. %s %s %s %s, %s + %s minuto %s minutos @@ -1117,6 +1137,7 @@ Detalhes da Conferência Gravações Conferência em curso + Critério de classificação %s %s, %s mais informações @@ -1143,6 +1164,7 @@ Retirar tudo do painel Adicionar ao painel Adicionar tudo ao painel + Conta Casa de família @@ -1151,28 +1173,50 @@ Recursos Bem-vindo, %1$s! Minhas Disciplinas - Ver Anúncios Anteriores Falha no carregamento da sala de estar Falha ao atualizar a sala de estar - - Nada Devido Hoje %1$s devido hoje %1$s em falta Bem-vindo! Os seus assuntos aparecem aqui. Atualmente, não tem assuntos. - - + Requer reinicialização da aplicação + Selecionar + Selecionar Período de classificação + Período de classificação atual + Falha ao carregar classificações + Falha no carregamento das classificações para o período de classificação + Falha em atualizar classificações + Sem classificações a exibir + Sem classificação + Período de classificação das alterações + Classificações não disponíveis + Casa de família + + Exibição da sala de aula + Ligações importantes + Aplicações para alunos + Informações de contato do pessoal + Escolha uma Disciplina + Ligações importantes + Professor + Assistente de ensino + Os seus recursos aparecem aqui. + Falha no carregamento de recursos + Falha na actualização de recursos Abrir uma vista alternativa mais acessível + Destinatários Assunto Seleccione uma disciplina, %s + As respostas deste aluno são ocultas porque esta tarefa é anónima. Anotação do aluno (Sem suporte) A anotação do aluno não é actualmente suportada em telemóvel. Tipo de apresentação não suportada A submissão não pode ser exibida, porque a Anotação do aluno não é atualmente suportada em telemóvel. + Marcou-o como feito. diff --git a/libs/pandares/src/main/res/values-ru/strings.xml b/libs/pandares/src/main/res/values-ru/strings.xml index 98e53c022c..c41f6aa27a 100644 --- a/libs/pandares/src/main/res/values-ru/strings.xml +++ b/libs/pandares/src/main/res/values-ru/strings.xml @@ -77,8 +77,8 @@ Показать параметры добавления файла Записать аудио Записать видео - Редактор текстовой отправки + Запись… Произошла ошибка при загрузке предоставляемых данных. Отправьте еще раз. @@ -102,10 +102,10 @@ Это задание не допускает отправку данных онлайн Это задание не допускает отправку данных онлайн Отправка данных онлайн невозможна - Это задание содержит ссылку на внешний инструмент для отправки. Открыть инструмент Текст для отправки + Из %s баллов Из %s баллов @@ -114,10 +114,10 @@ %1$s/%2$s %1$s из %2$s баллов Ввести оценку «что, если» - Низкий: %s Средний: %s Высокий: %s + Персонализированная оценка Нет раздела для этого задания @@ -201,7 +201,6 @@ Нет Заданий в этой группе Данное задание не обязательно и не будет учитываться в общем подсчете EX - Сортировать по Время Укажите @@ -213,6 +212,7 @@ Кнопка «Сортировать задания», сортировка по типу Задания, отсортированные по времени Назначения, отсортированные по типу + Баллы\u0020 Сохранить @@ -220,9 +220,9 @@ Ответы видны только тем, кто опубликовал не менее одного ответа. - Файл в настоящее время заблокирован Редактор ответов в обсуждении + Начинается Заканчивается @@ -460,6 +460,7 @@ Перестать действовать как пользователь Идентификатор пользователя Произошла ошибка при попытке действовать как пользователь + ID не может быть пустым Перейти к тестовому заданию @@ -616,6 +617,8 @@ Практические тесты Оцениваемые опросы Опросы + + Задачи Оценки @@ -661,12 +664,12 @@ Настройки профиля Параметры учетной записи PIN-код и отпечаток пальца - Сопряжение с наблюдателем Отсканировал ли ваш родитель этот QR-код из приложения Canvas Parent для сопряжения с вами. Срок действия этого кода истекает через семь дней или после однократного использования. Код сопряжения:\u0020 Ошибка кода сопряжения Невозможно получить код сопряжения. Эта функция поддерживается только для студентов. + Открыть Speedgrader для выполнения выполнить список заданий список задач @@ -737,8 +740,8 @@ SpeedGrader Gauge - Ползунок оценивания + Создать новое событие Выключить панд @@ -825,6 +828,7 @@ Получайте уведомления о готовности к записи конференции. Неограниченно + Вопрос Вопрос %d @@ -839,18 +843,19 @@ %s баллов %s баллов + Ограничение по времени Новые уведомления %1$s нравятся Добавление лайка нравится - %s лайк %s лайка(-ов) %s лайка(-ов) %s лайка(-ов) + Красный Ярко-розовый Лавандовый @@ -1016,10 +1021,10 @@ Конференции еще не поддерживаются на мобильных устройствах. Изображение для предварительного просмотра Ошибка при попытке скачать этот PDF. - Очень жаль! Эта функция недоступна в представлении для студентов. Здесь ничего нет для просмотра Неподдерживаемая функция + Добавить студента Введите полученный код привязки студента. @@ -1062,10 +1067,10 @@ Оценка после публикации Игнорировать оценку Текущая оценка - Открыть в Canvas Student Просмотр студентом + Выбранная голова: %s Выбранное тело: %s @@ -1137,6 +1142,7 @@ %s. %s %s %s %s, %s + %s минута %s минут @@ -1159,6 +1165,7 @@ Данные веб-конференции Записи Идет конференция + Оценка критерия %s %s, %s дополнительная информация @@ -1185,6 +1192,7 @@ Удалить все информационной панели Добавить на информационную панель Добавить все на информационную панель + Учетная запись «Домашняя комната» @@ -1193,28 +1201,50 @@ Ресурсы Добро пожаловать, %1$s! Мои темы - Просмотреть предыдущие объявления Не удалось загрузить «Домашнюю комнату» Не удалось обновить «Домашнюю комнату» - - Сегодня ничего сдавать не нужно Срок сдачи %1$s — сегодня %1$s отсутствует Добро пожаловать! Ваши темы будут отображаться здесь. В настоящее время у вас нет тем. - - + Требуется перезапуск приложения + Выбрать + Выбрать период оценивания + Текущий период оценки + Не удалось загрузить оценки + Не удалось загрузить оценки для академического периода + Не удалось обновить оценки + Нет оценок для отображения + Оценка не выставлена + Изменить академический период + Оценки недоступны + «Домашняя комната» + + Представление домашней комнаты + Важные ссылки + Заявки учащихся + Контактная информация персонала + Выбрать курс + Важные ссылки + Преподаватель + Ассистент преподавателя + Ваши ресурсы будут отображаться здесь. + Не удалось загрузить ресурсы + Не удалось обновить ресурсы Откройте более доступный альтернативный вид + Получатели Тема Выбрать курс, %s + Ответы студентов скрыты, поскольку это задание анонимно. Аннотация учащегося (не поддерживается) В настоящее время функция Аннотация учащегося не поддерживается на мобильных устройствах. Неподдерживаемый тип отправки Невозможно отобразить отправку, так как Аннотация учащегося в настоящее время не поддерживается на мобильном устройстве. + Вы отметили это как выполненное. diff --git a/libs/pandares/src/main/res/values-sl/strings.xml b/libs/pandares/src/main/res/values-sl/strings.xml index f928968b51..b809060cb3 100644 --- a/libs/pandares/src/main/res/values-sl/strings.xml +++ b/libs/pandares/src/main/res/values-sl/strings.xml @@ -15,7 +15,6 @@ ~ --> - Ni najdenih datotek. Poišči datoteke Vnesite iskalno besedo s tremi ali več znaki. @@ -33,6 +32,7 @@ Izbrano: %s + Podrobnosti o nalogi Vrste oddaj @@ -75,8 +75,8 @@ Prikaži možnosti za Dodaj datoteko Posnemi zvok Posnemi videoposnetek - Urejevalnik oddaje besedila + Zapis… Prišlo je do težav pri nalaganju oddaje. Ponovno pošlji. @@ -1046,6 +1046,7 @@ + Izbrana glava: %s Izbrano telo: %s Izbrane noge: %s @@ -1164,6 +1165,7 @@ Odstrani vse z nadzorne plošče Dodaj na nadzorno ploščo Dodaj vse na nadzorno ploščo + Račun Matični @@ -1172,28 +1174,50 @@ Viri Dobrodošli, %1$s! Moji kratki opisi predmetov - Prikaži predhodna obvestila Nalaganje matičnega razreda ni uspelo Osveževanje matičnega razreda ni uspelo - - Danes nič ne poteče %1$s poteče danes %1$s manjka Dobrodošli. Vaši predmeti se prikažejo tukaj. Trenutno nimate premetov. - + Zahteva ponovni zagon aplikacije + Izberi + Izberi ocenjevalno obdobje + Trenutno ocenjevalno obdobje + Nalaganje ocen ni uspelo. + Ocen za obdobje ocenjevanja ni bilo mogoče naložiti + Osveževanje ocen ni uspelo + Ni ocen za prikaz + Ni ocenjeno + Sprememba ocenjevalnega obdobja + Ocene niso na voljo + Matični razred + Pogled matičnega razreda + Pomembne povezave + Vloge študentov + Kontaktni podatki osebja + Izberite predmet + Pomembne povezave + Izvajalec + Demonstrator + Tukaj se prikažejo vaši viri. + Nalaganje virov ni uspelo + Osveževanje virov ni uspelo Odpri dostopnejši alternativni pogled + Prejemniki Zadeva Izberite predmet, %s + Odgovori tega študenta so skriti, ker je ta naloga anonimna. Opomba študenta (ni podprto) Opomba študenta v mobilnem prikazu trenutno ni podprta. Nepodprta vrsta oddaje Oddaje ni mogoče prikazati, ker funkcija Opomba študenta v mobilnem prikazu trenutno ni podprta. + To ste označili kot končano. diff --git a/libs/pandares/src/main/res/values-sv/strings.xml b/libs/pandares/src/main/res/values-sv/strings.xml index b106d33c22..5d11231ec2 100644 --- a/libs/pandares/src/main/res/values-sv/strings.xml +++ b/libs/pandares/src/main/res/values-sv/strings.xml @@ -58,7 +58,6 @@ Startar externt verktyg… Starta externt verktyg Förhandsgranska - En eller fler filer laddades inte upp. Kontrollera din internetanslutning och försök lämna in igen. %1$s av %2$s Inlämningen slutfördes! @@ -68,20 +67,20 @@ Inlämningen borttagen Saknas Har bedömts + Ingen förhandsgranskning är tillgänglig för URL:er som använder \'http://\' Ange en giltig URL Ange en URL här för din inlämning Visa alternativ för Lägg till fil Spela in ljud - Spela in video - Textinlämningsredigerare + Skriv… - Något gick fel vid uppladdning av inlämningen. Lämna in igen. + Inskickning Inskickningsversioner @@ -98,25 +97,25 @@ Inlämning inte tillåten Förhandsgranskning av angiven URL Webbplatsens URL - Den här uppgiften tillåter inte onlineinlämningar Den här uppgiften tillåter inte onlineinlämningar Inga onlineinlämningar Den här uppgiften länkar till ett externt verktyg för inlämningar. Öppna verktyg Inlämningstext + Av %s poäng Av %s poäng Ursäktad Slutbedömning: %s - %1$s/%2$s %1$s av %2$s poäng Ange Vad om-resultat Låg: %s Medel: %s Hög: %s + Anpassat resultat Det finns ingen matris för den här uppgiften @@ -128,7 +127,7 @@ Laddar upp mediefil Uppladdningen av kommentaren misslyckades för %s Bifoga filer till dina kommentarer genom att välja ett alternativ nedan - Har du frågor om din uppgift?\nSkicka ett meddelande till din instruktör. + Har du frågor om din uppgift?\nSkicka ett meddelande till din lärare. Detta meddelande kunde inte skickas. Tryck för att försöka igen. Medieuppladdning Mediauppladdning – ljud @@ -141,6 +140,7 @@ Mediafil Ljud Video + Version: @@ -210,15 +210,17 @@ Sortera uppgiftsknappar, sortera efter typ Uppgifter sorterade efter tid Uppgifter sorterade efter typ + Poäng\u0020 Spara Avbryt + Svaren är endast synliga för de som har publicerat minst ett svar. - Den här filen är för närvarande låst Diskussionssvarsredigerare + Börjar Slutar @@ -337,7 +339,7 @@ Öppna Öppna med en alternativ app Öppnar fil… - Ladda ned + Ladda ner Meddelandebilagor Bilaga Bilageikon @@ -353,7 +355,7 @@ Välj en kurs eller grupp Inga meddelanden Ta bort bilaga - Ladda ned bilaga + Ladda ner bilaga Meddelandealternativ Vidarebefordra Svara alla @@ -429,6 +431,7 @@ Borttagen + Läser in Canvas-innehåll… UnknownDevice @@ -445,9 +448,11 @@ Sluta uppträda som användare Användar-ID Ett fel inträffade vid försök att uppträda som användaren. + ID kan inte lämnas tomt Gå till quiz + Quiz Senaste inlägg @@ -525,9 +530,9 @@ Denna inskickning accepterar endast en filuppladdning Lägg till webbplatsinlägg Mediainspelningar + Skicka - Skicka Osparat arbete Osparad information kommer att förloras. Vill du fortsätta? @@ -542,6 +547,7 @@ Nästa Lägg till en kommentar… + Startsida Aviseringar @@ -571,6 +577,8 @@ En ögonblicksbild av webbplatsen togs när du lämnade in den. Tryck och håll på bilden nedan för att öppna eller ladda ner hela bilden. Den här inskickningen var en URL till en extern sida. Kom ihåg att den här sidan kan ha ändrats sedan inlämningen ursprungligen gjordes. Förhandsvisning av den skickade URL:en + + %1$s stöds inte. Länken stöds inte. Öppna i webbläsare @@ -586,15 +594,19 @@ Panda-fakta:  Ta bort Grundare + EULA Integritetspolicy Användningsvillkor Canvas på GitHub + Uppgiftsquiz Övningsquiz Bedömda enkäter Enkäter + + Att göra Omdömen @@ -640,12 +652,12 @@ Profilinställningar Kontoinställningar PIN och fingeravtryck - Koppla med observatör Be din förälder scanna QR-koden från Canvas Parent-appen för att parkopplas med dig. Den här koden upphör att gälla om sju dagar, eller efter en användning. Parkopplingskod:\u0020 Parkopplingskodfel Det gick inte att hämta en parkopplingskod. Den här funktionen stöds endast för studenter. + Öppna Speedgrader att göra att göra lista @@ -716,8 +728,8 @@ SpeedGrader Mätare - Omdömesreglage + Skapa ny händelse Stäng av pandorna @@ -735,7 +747,7 @@ Kursaktiviteter Diskussioner - Konversationer + Inkorgen Schemaläggning Grupper Larm @@ -757,7 +769,7 @@ Diskussionsinlägg Lägg till i konversation - Konversationsmeddelande + Meddelande i Inkorgen Konversationer som skapats av dig Studentens mötesregistreringar @@ -786,24 +798,25 @@ Få aviseringar när du skapar ett anslag, och när någon svarar på ditt anslag. Få aviseringar när en uppgift/inlämning har bedömts/ändrats, och när en omdömesvikt ändrats. Få aviseringar om inbjudningar till webbkonferenser, grupper, samarbeten, kamratresponsuppgifter och påminnelser. - Endast lärare och admin. Få aviseringar när en uppgift lämnas in för första gången eller lämnas in på nytt. - Endast lärare och admin. Få aviseringar när en sen uppgift skickas in. + Endast lärare och administratörer. Få aviseringar när en uppgift lämnas in för första gången eller lämnas in på nytt. + Endast lärare och administratörer. Få aviseringar när en sen uppgift skickas in. Få aviseringar när en kommentar görs om din inlämning. Få aviseringar när det finns ett nytt diskussionsämne i din kurs. Få aviseringar när det finns ett nytt inlägg i en diskussion som du prenumererar på. Få aviseringar när du läggs till i en konversation. Få aviseringar när du har ett nytt meddelande i din inkorg. Få aviseringar när du skapar en ny konversation. - Endast lärare och admin. Få aviseringar när det finns en mötesregistrering. + Endast lärare och administratörer. Få aviseringar när det finns en mötesregistrering. Få aviseringar när det finns en ny registrering på din kalender. Få aviseringar vid avbeställning av tid. Få aviseringar när en möteslucka blir tillgänglig. Få aviseringar om nya och uppdaterade kalenderobjekt. Endast admin, väntande registrering aktiverad. Få avisering när en gruppregistrering accepteras eller nekas. - Endast lärare och admin. Få aviseringar om kursregistreringar, genererade rapporter, exporterat innehåll, migrationsrapporter, nya kontoanvändare, och nya studentgrupper. + Endast lärare och administratörer. Få aviseringar om kursregistreringar, genererade rapporter, exporterat innehåll, migrationsrapporter, nya kontoanvändare, och nya studentgrupper. Få aviseringar när en konferensinspelning är klar. Obegränsade + Fråga %d fråga @@ -814,16 +827,17 @@ %s poäng %s poäng + Tidsgräns %1$s Nya aviseringar gilla-markering Gilla post gilla-markeringar - %s gilla-markering %s gilla-markeringar + Röd Klarrosa Lavendel @@ -972,6 +986,7 @@ Vi kunde inte hitta någon extern app för visning av det här LTI-verktyget. Ladda upp kommentar + Förstasidan Redigera sidan Beskrivning @@ -984,21 +999,21 @@ Konferenser stöds ännu inte på den här mobilen. Förhandsvisningsbild för fil Ett fel uppstod vid inläsning av den här PDF:en. - Tyvärr! Den här funktionen tillåts inte i studentvyn. Inget att se här Funktionen saknar stöd + Lägg till student Ange kopplingskoden du fått för studenter. Kopplingskod ... Det gick inte att koppla. Se till att din kopplingskod är korrekt och att dess tidsgräns inte har överskridits. Färdig - ofullständig + Inte färdig Publiceringsinställningar - Offentliggör omdömen + Publicera omdömen Dölj omdömen %d omdöme publicerat för närvarande @@ -1020,7 +1035,7 @@ Alla omdömen har publicerats. Publicerade omdömen Dolda omdömen - Det gick inte att offentliggör omdömen + Det gick inte att publicera omdömen Det gick inte att dölja omdömen Bedöm före publicering Bedöm efter publicering @@ -1029,6 +1044,7 @@ Öppnas i Canvas Student Studentens vy + Vald rubrik: %s Vald brödtext: %s @@ -1082,7 +1098,6 @@ Lägg till videokommentar Lägg till ljudkommentar - Ett oväntat fel inträffade under ljudinspelningen. Ett oväntat fel inträffade under videoinspelningen. @@ -1101,6 +1116,7 @@ %s. %s %s %s %s, %s + %s minut %s minuter @@ -1121,6 +1137,7 @@ Konferensinformation Inspelningar Konferens pågår... + Kriteriebedömning %s %s, %s mer information @@ -1147,6 +1164,7 @@ Ta bort alla från översikten Lägg till i översikten Lägga till alla i översikten + Konto Fjärrundervisningsrum @@ -1155,28 +1173,50 @@ Resurser Välkommen %1$s! Mina ämnen - Visa tidigare meddelanden Det gick inte att läsa in fjärrundervisningsrummet. Det gick inte att uppdatera fjärrundervisningsrummet. - - Inget måste lämnas in i dag %1$s måste lämnas in i dag %1$s saknas Välkommen! Dina ämnen visas här. Du har inga ämnen för närvarande. - - + Kräver omstart av appen + Välj + Välj omdömesperiod + Aktuell omdömesperiod + Det gick inte att läsa in omdömen + Det gick inte att läsa in omdömen för omdömesperioden + Det gick inte att uppdatera omdömen + Det finns inga omdömen att visa + Ej bedömd + Ändra omdömesperiod + Omdömen är inte tillgängliga + Fjärrundervisningsrum + + Vy för fjärrundervisningsrum + Viktiga länkar + Studentansökningar + Kontaktuppgifter för personal + Välj en kurs + Viktiga länkar + Lärare + Lärarassistent + Dina resurser visas här. + Det gick inte att läsa in resurser + Det gick inte att uppdatera resurser Öppna en mer tillgänglig alternativ vy + Mottagare Ämne Välj en kurs, %s + Den här studentens svar är dolda eftersom uppgiften är anonym. Studentnotering (stöds inte) Studentnotering stöds för närvarande inte på mobila enheter. Inlämningstyp som inte stöds Inlämningen kan inte visas eftersom studentnotering inte stöds på mobila enheter. + Du har markerat den som färdig. diff --git a/libs/pandares/src/main/res/values-th/strings.xml b/libs/pandares/src/main/res/values-th/strings.xml index 0076f54a19..0a2d079022 100644 --- a/libs/pandares/src/main/res/values-th/strings.xml +++ b/libs/pandares/src/main/res/values-th/strings.xml @@ -1182,7 +1182,30 @@ ยินดีต้อนรับ! วิชาของคุณจะปรากฏขึ้นที่นี่ ปัจจุบันคุณไม่มีวิชาใด ๆ - + ต้องรีสตาร์ทแอพ + เลือก + เลือกระยะเวลาการให้เกรด + ระยะเวลาการให้เกรดในปัจจุบัน + ไม่สามารถโหลดเกรดได้ + ไม่สามารถโหลดเกรดสำหรับระยะเวลาให้เกรด + ไม่สามารถรีเฟรชเกรดได้ + ไม่มีเกรดที่จะแสดง + ไม่ได้ลงเกรด + เปลี่ยนแปลงระยะเวลาการให้เกรด + ไม่มีเกรด + โฮมรูม + + มุมมองโฮมรูม + ลิงค์ที่สำคัญ + แอพพลิเคชั่นของผู้เรียน + ข้อมูลติดต่อบุคลากร + เลือกบทเรียน + ลิงค์ที่สำคัญ + ผู้สอน + ผู้ช่วยสอน + ทรัพยากรของคุณจะปรากฏขึ้นที่นี่ + ไม่สามารถโหลดทรัพยากรได้ + ไม่สามารถรีเฟรชทรัพยากรได้ เปิดมุมมองเผื่อเลือกที่สามารถเข้าถึงได้มากกว่า ผู้รับ @@ -1195,4 +1218,5 @@ หมายเหตุกำกับของผู้เรียนปัจจุบันไม่รองรับสำหรับอุปกรณ์พกพา ประเภทการจัดส่งที่ไม่รองรับ ไม่สามารถแสดงผลงานจัดส่งได้เนื่องจากหมายเหตุกำกับของผู้เรียนปัจจุบันไม่รองรับกับอุปกรณ์พกพา + คุณกำกับว่าเสร็จสิ้นแล้ว diff --git a/libs/pandares/src/main/res/values-zh-rHK/strings.xml b/libs/pandares/src/main/res/values-zh-rHK/strings.xml index b1b313ee1a..2300ba721c 100644 --- a/libs/pandares/src/main/res/values-zh-rHK/strings.xml +++ b/libs/pandares/src/main/res/values-zh-rHK/strings.xml @@ -54,10 +54,8 @@ 已使用的遞交嘗試次數 不可再次嘗試 重新提交作業 - 啟動外部工具中… 啟動外部工具 - 預覽 上傳一個或多個檔案失敗。檢查您的網際網路連線並重試提交。 %1$s / %2$s @@ -68,19 +66,20 @@ 已刪除提交項目 缺少 已評分 + 沒有使用 \'http://\' 的 URL 的可用預覽 請輸入有效的 URL 請在此輸入您的提交項目的 URL 顯示添加檔案選項 錄製音訊 - 錄製視訊 - 文字提交項目編輯器 + 寫入… 上傳提交項目時出現問題。重新提交。 + 提交項目 提交項目版本 @@ -103,6 +102,7 @@ 此作業已連結至外部工具以供提交。 開啟工具 提交項目文字 + 滿分為 %s 分 滿分為 %s 分 @@ -114,10 +114,12 @@ 低:%s 平均:%s 高:%s + 自訂分數 此作業沒有評分標準 檢視完整說明 + 正在上傳 %1$d / %2$d 檔案 正在上傳 %s 的評論 @@ -137,6 +139,7 @@ 媒體檔案 音訊 視訊 + 版本: @@ -186,6 +189,8 @@ 截止於 %1$s 的 %2$s 上午 下午 + + 已鎖定 作業列表 @@ -204,15 +209,17 @@ 排列作業列表按鈕,依類型排序 依時間排序的作業列表 依類型排序的作業列表 + 分數\u0020 儲存 取消 + 只有曾發送一個或以上回覆的才能查閱回覆。 - 此檔案目前已鎖定 討論區回覆編輯器 + 開始 結束 @@ -231,8 +238,6 @@ 可能得分 %1$s/%2$s (%3$s) 得分為 %1$s,滿分為 %2$s,%3$s - - 收件匣 未讀 已存檔 @@ -393,8 +398,8 @@ 學生 觀察者 尚未添加課程大綱。 - 載入單元時發生錯誤。 + Canvas 選擇收件人 本訊息目前無收件人。 @@ -420,6 +425,7 @@ 已刪除 + 載入 Canvas 內容… UnknownDevice @@ -436,9 +442,11 @@ 停止作為使用者 使用者 ID 嘗試作為使用者時出現錯誤 + ID 不可為空白 前往測驗 + 測驗 最後發布 @@ -497,8 +505,8 @@ 前往單元 標記為已完成 無法找到單元項目 - 載入單元時發生錯誤。 + 支援 導師提問 連結 @@ -514,11 +522,11 @@ 文字條目 線上 URL 此提交項目只接受上載一個檔案 - - 添加網站條目 媒體錄製 提交 + + 進度未儲存 未儲存的資料將丟失。您確定要繼續嗎? @@ -533,6 +541,7 @@ 下一個 添加評論… + 首頁 通知 @@ -562,6 +571,8 @@ 您提交時的網站截圖已儲存。點選並抓取下方圖片,或下載完整圖片。 此提交項目是連結至外部頁面的 URL。請謹記,自從最初提交之後,此頁面可能已變更。 預覽已提交的 url + + 不支援 %1$s。 不支援連結。 在瀏覽器中打開 @@ -577,15 +588,19 @@ 關於熊貓的事實:  移除 創始人 + EULA 隱私政策 使用條款 GitHub 上的 Canvas + 作業測驗 練習測驗 已評分的調查 調查 + + 待辦事項 成績 @@ -631,12 +646,12 @@ 個人檔案設定 帳戶偏好 PIN 和指紋 - 與觀察者配對 讓您的父母在 Canvas Parent 應用程式掃描此二維碼與您配對。此代碼將在七天後過期,或使用一次後過期。 配對代碼:\u0020 配對代碼錯誤 無法取得配對代碼。此功能僅支援學生。 + 打開 Speedgrader 待辦事項清單 @@ -707,8 +722,8 @@ SpeedGrader Gauge - 評分滑條 + 創建新活動 關閉熊貓頭像 @@ -795,6 +810,7 @@ 在會議錄製準備就緒時收到通知。 無限制 + 問題 %d 個問題 @@ -803,15 +819,16 @@ %s 分 + 時間限制 %1$s 個新通知 「讚好」條目 - %s 讚好 + 紅色 亮粉色 薰衣草色 @@ -870,6 +887,7 @@ 編輯控制面板 選擇要在控制面板上查看哪些課程。 編輯您的課程清單 + 控制面板 上一個 第 %d 頁,共 %d 頁 @@ -897,8 +915,8 @@ 點選檢視通告 接受 拒絕 - 滿分為 + 沒有與此課程關聯的檔案。 沒有與此群組關聯的檔案。 @@ -957,6 +975,7 @@ 我們未能找到外部應用程式以檢視此 LTI 工具。 上載評論 + 標題頁 編輯內容頁 說明 @@ -969,10 +988,10 @@ 移動設備尚未支援會議。 檔案預覽圖像 嘗試載入此 PDF 時發生錯誤。 - 很抱歉!此功能無法於學生視圖中使用。 這裡沒有內容供瀏覽 不支援的功能 + 添加學生 輸入已提供予您的學生配對代碼。 @@ -980,6 +999,7 @@ 配對失敗。請確保您的配對代碼正確,並在使用限期內。 完成 未完成 + 公佈設定 公佈成績 @@ -1008,10 +1028,10 @@ 公佈後評分 評分重寫 目前評分 - 在 Canvas Student 中開啟 學生視圖 + 選擇的頁首:%s 選擇的正文:%s @@ -1065,7 +1085,6 @@ 添加視訊評論 添加音訊評論 - 在嘗試錄製音訊時發生意外錯誤。 在嘗試錄製視訊時發生意外錯誤。 @@ -1084,6 +1103,7 @@ %s。 %s %s %s %s, %s + %s 分鐘 @@ -1103,6 +1123,7 @@ 會議詳情 記錄 會議進行中 + 標準評級%s %s,%s 更多資訊 @@ -1129,6 +1150,7 @@ 全部從控制面板移除 添加到控制面板中 全部添加到控制面板中 + 帳戶 主教室 @@ -1137,28 +1159,50 @@ 資源 歡迎,%1$s! 我的主題 - 檢視之前的通告 未能載入主教室 未能重新整理主教室 - - 今天沒有任何內容截止 %1$s 今天截止 %1$s 缺失 歡迎! 您的主題在此顯示。 您目前沒有任何主題。 - - + 需要重新啟動應用程式 + 選擇 + 選擇評分期 + 目前評分期 + 無法載入成績 + 無法載入評分期的成績 + 無法重新整理成績 + 沒有可顯示的成績 + 未評分 + 變更評分期 + 不提供成績 + 主教室 + + 主教室檢視 + 重要連結 + 學生應用程式 + 員工聯絡資料 + 選擇一個課程 + 重要連結 + 教師 + 助教 + 您的資源在此顯示。 + 無法載入資源 + 無法重新整理資源 開啟更容易存取的替代視圖 + 收件人 主題 選擇課程,%s + 這名學生的回覆已隱藏,因為此作業為匿名狀態。 學生註釋(不支援) 流動電話目前不支援學生註釋。 不支援提交項目類型 提交項目無法顯示,因為流動電話目前不支援學生註釋。 + 您已將其標示為完成。 diff --git a/libs/pandares/src/main/res/values-zh/strings.xml b/libs/pandares/src/main/res/values-zh/strings.xml index f03b94c3d1..07c0fdf329 100644 --- a/libs/pandares/src/main/res/values-zh/strings.xml +++ b/libs/pandares/src/main/res/values-zh/strings.xml @@ -54,10 +54,8 @@ 已尝试的次数 尝试次数已用完 重新提交作业 - 正在启动外部工具… 启动外部工具 - 预览 一份或多份文件上传失败。请检查您的网络连接,并重试提交。 %1$s/%2$s @@ -68,19 +66,20 @@ 提交项已删除 缺失 已评分 + 使用“http://”的 URL 无可用预览 请输入有效的 URL 请在此处为您的提交项输入 URL 显示添加文件选项 录制音频 - 录制视频 - 文本提交项编辑器 + 写入… 上传提交项时出错。请重新提交。 + 提交 提交版本 @@ -103,6 +102,7 @@ 此作业关联用于提交的外部工具。 打开工具 提交项文本 + 满分 %s 满分 %s @@ -114,10 +114,12 @@ 低:%s 中等:%s 高:%s + 自定义评分 此作业没有评估表格 查看长说明 + 正在上传%2$d的文件%1$d 正在上传关于%s的评论 @@ -137,6 +139,7 @@ 媒体文件 音频 视频 + 版本: @@ -186,6 +189,8 @@ 截止于 %1$s,%2$s 上午 下午 + + 已锁定 作业 @@ -204,15 +209,17 @@ 作业排序按钮,按类型排序 作业按时间排序 作业按类型排序 + 得分\u0020 保存 取消 + 回复只对至少发布了一条回复的人可见。 - 此文件目前被锁定 讨论回复编辑器 + 开始 结束 @@ -231,8 +238,6 @@ 假设的分数 %1$s/%2$s (%3$s) 得分 %1$s,总分 %2$s,%3$s - - 收件箱 未读 已归档 @@ -393,8 +398,8 @@ 学生 观察员 未添加教学大纲。 - 加载单元时出错。 + Canvas 选择收件人(s) 这个消息目前还没有收件人。 @@ -420,6 +425,7 @@ 已删除 + 正在加载Canvas内容… UnknownDevice @@ -436,9 +442,11 @@ 停止以用户的身份执行操作 用户ID 尝试以用户的身份执行操作时出现错误 + 该ID不能为空白 转到测验 + 测验 上一次发布 @@ -497,8 +505,8 @@ 转到单元 标记为完成 找不到单元项目 - 加载单元时出错。 + 帮助 讲师的问题 链接 @@ -514,11 +522,11 @@ 文本输入 在线 URL 此提交项只接受上传一份文件 - - 添加网站条目 媒体录音 提交 + + 未保存的进度 未保存的信息将会丢失。是否要继续? @@ -533,6 +541,7 @@ 下一步 添加评论… + 首页 通知 @@ -562,6 +571,8 @@ 当您打开该网站时,它就会拍摄快照。点击并按住下面的图片,打开或下载完整的图像。 此提交是指向外部页面的一个 URL。请记住,此页面自最初进行提交后可能已更改。 提交URL的预览 + + %1$s不受支持。 链接不受支持。 在浏览器里打开 @@ -577,15 +588,19 @@ 熊猫资料:  移除 创始人 + 最终用户许可协议 隐私政策 使用条款 GitHub上的Canvas + 作业测验 练习测验 评分调查 调查 + + 待办事项 评分 @@ -631,12 +646,12 @@ 个人资料设置 帐户首选项 PIN和指纹 - 与旁听者配对 您的父母是否已从 Canvas Parent 应用程序扫描此二维码与您配对?该代码将在七天后或使用一次后失效。 配对码:\u0020 配对码错误 无法获取配对码。该功能仅支持学生使用。 + 打开 Speedgrader 待办事项 待办事项的待办列表 @@ -707,8 +722,8 @@ SpeedGrader Gauge - 评分滑块 + 创建新事件 关闭熊猫 @@ -795,6 +810,7 @@ 当会议录制准备就绪时收到通知。 无限制 + 问题 %d 个问题 @@ -803,15 +819,16 @@ %s 分 + 时间限制 %1$s 条新通知 喜欢 点赞 喜欢 - %s 赞 + 红色 亮粉色 薰衣草 @@ -870,6 +887,7 @@ 编辑控制面板 选择您想在控制面板上查看哪些课程 编辑您的课程列表 + 控制面板 上一个 第 %d 页,共 %d 页 @@ -897,8 +915,8 @@ 单击以查看公告 接受 拒绝 - + 没有与此课程关联的文件。 没有与此小组关联的文件。 @@ -957,6 +975,7 @@ 我们找不到查看此 LTI 工具的外部应用。 评论上传 + 首页 编辑页面 说明 @@ -969,10 +988,10 @@ 会议目前不支持移动设备。 文件预览图像 尝试加载此PDF时出错。 - 抱歉!该功能不允许在学生视图中使用。 此处看不到任何内容 不受支持的功能 + 添加学生 输入提供给您的学生配对码。 @@ -980,6 +999,7 @@ 配对失败。确保配对码正确,且未超过使用时间限制。 完成 未完成 + 发布设置 发布评分 @@ -1008,10 +1028,10 @@ 发布后的评分 覆盖评分 当前评分 - 在 Canvas 学生版中打开 学生视图 + 已选头部:%s 已选身体:%s @@ -1065,7 +1085,6 @@ 添加视频评论 添加音频评论 - 尝试录制音频时发生意外错误。 尝试录制视频时发生意外错误。 @@ -1084,6 +1103,7 @@ %s。 %s %s %s %s,%s + %s 分钟 @@ -1103,6 +1123,7 @@ 会议详情 录制文件 会议进行中 + 标准评分 %s %s,%s 更多信息 @@ -1129,6 +1150,7 @@ 全部从控制面板删除 添加到控制面板 全部添加到控制面板 + 帐户 指导教室 @@ -1137,28 +1159,50 @@ 资源 欢迎您,%1$s! 我的主题 - 查看以前的公告 无法加载 Homeroom 无法刷新 Homeroom - - 没有今天到期的项目 %1$s个今天到期 %1$s个缺失 欢迎! 您的科目显示在这里。 您目前没有科目。 - - + 要求重启 app + 选择 + 选择评分周期 + 当前评分周期 + 加载评分失败 + 无法加载评分周期的评分 + 无法刷新评分 + 没有要显示的评分 + 未评分 + 更改评分周期 + 评分不可用 + 指导教室 + + 教室视图 + 重要链接 + 学生申请 + 员工联系信息 + 选择课程 + 重要链接 + 教师 + 助教 + 您的资源显示在此处。 + 无法加载资源 + 无法刷新资源 打开一个更加方便访问的备选视图 + 收件人 主题 选择课程 %s + 该学生的回答已隐藏,因为该作业已匿名。 学生注释(不支持) 目前在移动设备上不支持学生注释。 不受支持的提交项类型 无法显示提交项,因为目前在移动设备上不支持学生注释。 + 您已将其标记为“完成”。 diff --git a/libs/pandares/src/main/res/values/colors.xml b/libs/pandares/src/main/res/values/colors.xml index edde1d655e..8a746ed3fd 100644 --- a/libs/pandares/src/main/res/values/colors.xml +++ b/libs/pandares/src/main/res/values/colors.xml @@ -255,6 +255,6 @@ #008EE2 #2D3B45 - #C6C6C8 + #C6C6C8 diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index f40b5b67eb..e18df11006 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -1183,8 +1183,29 @@ Welcome! Your subjects show up here. You currently have no subjects. - Elementary View Requires app restart + Select + Select Grading Period + Current Grading Period + Failed to load grades + Failed to load grades for grading period + Failed to refresh grades + No grades to display + Not Graded + Change grading period + Grades not available + Homeroom + Homeroom View + Important Links + Student Applications + Staff Contact Info + Choose a Course + Important Links + Teacher + Teaching Assistant + Your resources show up here. + Failed to load resources + Failed to refresh resources Open a more accessible alternative view @@ -1198,4 +1219,9 @@ Student Annotation is currently not supported on mobile. Unsupported Submission Type Submission cannot be displayed, because Student Annotation is currently not supported on mobile. + You\'ve marked it as done. + Cannot change the due date when due in a closed grading period. + Jump to Today + %1$s, %2$s + send message diff --git a/libs/pandautils/build.gradle b/libs/pandautils/build.gradle index 3b92bd3eac..bca1702607 100644 --- a/libs/pandautils/build.gradle +++ b/libs/pandautils/build.gradle @@ -163,4 +163,6 @@ dependencies { implementation Libs.VIEW_MODE_SAVED_STATE implementation Libs.FRAGMENT_KTX kapt Libs.LIFECYCLE_COMPILER + + implementation 'com.google.android.flexbox:flexbox:3.0.0' } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/adapters/itemdecorations/StickyHeaderInterface.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/adapters/itemdecorations/StickyHeaderInterface.kt new file mode 100644 index 0000000000..c3d3a9e1c6 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/adapters/itemdecorations/StickyHeaderInterface.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.adapters.itemdecorations + +import androidx.databinding.ViewDataBinding +import androidx.recyclerview.widget.RecyclerView + +interface StickyHeaderInterface { + + fun getHeaderPositionForItem(itemPosition: Int): Int + + fun getHeaderBinding(headerPosition: Int, parent: RecyclerView, hasChildInContact: Boolean): ViewDataBinding + + fun isHeader(itemPosition: Int): Boolean +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/adapters/itemdecorations/StickyHeaderItemDecoration.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/adapters/itemdecorations/StickyHeaderItemDecoration.kt new file mode 100644 index 0000000000..33a436811d --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/adapters/itemdecorations/StickyHeaderItemDecoration.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.adapters.itemdecorations + +import android.graphics.Canvas +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView + +class StickyHeaderItemDecoration(private val stickyHeaderAdapter: StickyHeaderInterface) : + RecyclerView.ItemDecoration() { + + private var stickyHeaderHeight = 0 + + private var xPos = 0 + + private var contactPointPair: Pair? = null + private var childInContact: View? = null + + override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + super.onDrawOver(c, parent, state) + val topChild = parent.getChildAt(0) ?: return + val topChildPosition = parent.getChildAdapterPosition(topChild) + if (topChildPosition == RecyclerView.NO_POSITION) { + return + } + + val headerPos: Int = stickyHeaderAdapter.getHeaderPositionForItem(topChildPosition) + if (contactPointPair != null && contactPointPair?.first == headerPos) { + childInContact = getChildInContact(parent, contactPointPair!!.second, contactPointPair!!.first) + } + + val currentHeader = + if (childInContact != null && !stickyHeaderAdapter.isHeader(parent.getChildAdapterPosition(childInContact!!))) { + getHeaderViewForItem(headerPos, parent, true) + } else { + getHeaderViewForItem(headerPos, parent, false) + } + fixLayoutSize(parent, currentHeader) + + val contactPoint = currentHeader.bottom + contactPointPair = Pair(headerPos, contactPoint) + if (childInContact != null && stickyHeaderAdapter.isHeader(parent.getChildAdapterPosition(childInContact!!))) { + moveHeader(c, currentHeader, childInContact!!) + return + } + drawHeader(c, currentHeader) + } + + private fun getHeaderViewForItem(headerPosition: Int, parent: RecyclerView, hasChildInContact: Boolean = false): View { + val binding = stickyHeaderAdapter.getHeaderBinding(headerPosition, parent, hasChildInContact) + binding.executePendingBindings() + return binding.root + } + + private fun drawHeader(canvas: Canvas, header: View) { + canvas.save() + canvas.translate(xPos.toFloat(), 0f) + header.draw(canvas) + canvas.restore() + } + + private fun moveHeader(canvas: Canvas, currentHeader: View, nextHeader: View) { + canvas.save() + canvas.translate(xPos.toFloat(), (nextHeader.top - currentHeader.height).toFloat()) + currentHeader.draw(canvas) + canvas.restore() + } + + private fun getChildInContact(parent: RecyclerView, contactPoint: Int, currentHeaderPos: Int): View? { + var childInContact: View? = null + for (i in 0 until parent.childCount) { + var heightTolerance = 0 + val child = parent.getChildAt(i) + + if (currentHeaderPos != i) { + val childAdapterPosition = parent.getChildAdapterPosition(child) + val isChildHeader: Boolean = stickyHeaderAdapter.isHeader(childAdapterPosition) + if (isChildHeader) { + heightTolerance = stickyHeaderHeight - child.height + } + } + + val childBottomPosition: Int = if (child.top > 0) { + child.bottom + heightTolerance + } else { + child.bottom + } + if (childBottomPosition > contactPoint) { + if (child.top <= contactPoint) { + childInContact = child + break + } + } + } + return childInContact + } + + private fun fixLayoutSize(parent: ViewGroup, view: View) { + + val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY) + val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED) + + val childWidthSpec = + ViewGroup.getChildMeasureSpec(widthSpec, parent.paddingLeft + parent.paddingRight, view.layoutParams.width) + val childHeightSpec = ViewGroup.getChildMeasureSpec( + heightSpec, + parent.paddingTop, + view.layoutParams.height + ) + xPos = parent.paddingLeft + view.measure(childWidthSpec, childHeightSpec) + view.layout(0, 0, view.measuredWidth, view.measuredHeight.also { stickyHeaderHeight = it }) + } + +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindableRecyclerViewAdapter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindableRecyclerViewAdapter.kt index ee5dc43f75..68156ea7ad 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindableRecyclerViewAdapter.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindableRecyclerViewAdapter.kt @@ -19,19 +19,32 @@ package com.instructure.pandautils.binding import android.view.LayoutInflater import android.view.ViewGroup import androidx.databinding.DataBindingUtil +import androidx.databinding.Observable import androidx.databinding.ViewDataBinding -import androidx.databinding.library.baseAdapters.BR import androidx.recyclerview.widget.RecyclerView +import com.instructure.pandautils.BR import com.instructure.pandautils.mvvm.ItemViewModel -class BindableRecyclerViewAdapter : RecyclerView.Adapter() { +open class BindableRecyclerViewAdapter : RecyclerView.Adapter() { - var itemViewModels: List = emptyList() + var itemViewModels: MutableList = mutableListOf() private val viewTypeLayoutMap: MutableMap = mutableMapOf() + private val groupObserver = object : Observable.OnPropertyChangedCallback() { + override fun onPropertyChanged(sender: Observable?, propertyId: Int) { + if (sender is GroupItemViewModel && propertyId == BR.collapsed) { + toggleGroup(sender) + } + } + + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindableViewHolder { - val binding: ViewDataBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), viewTypeLayoutMap[viewType] - ?: 0, parent, false) + val binding: ViewDataBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), viewTypeLayoutMap[viewType] + ?: 0, parent, false + ) + return BindableViewHolder(binding) } @@ -50,10 +63,36 @@ class BindableRecyclerViewAdapter : RecyclerView.Adapter() { } fun updateItems(items: List?) { - itemViewModels = items ?: emptyList() + itemViewModels = items.orEmpty().toMutableList() + val groups = itemViewModels.filterIsInstance() + setupGroups(groups) notifyDataSetChanged() } + private fun setupGroups(groups: List) { + groups.forEach { + it.removeOnPropertyChangedCallback(groupObserver) + it.addOnPropertyChangedCallback(groupObserver) + if (!it.collapsed) { + itemViewModels.addAll(itemViewModels.indexOf(it) + 1, it.items) + notifyItemRangeInserted(itemViewModels.indexOf(it) + 1, it.items.size) + setupGroups(it.items.filterIsInstance()) + } + } + } + + private fun toggleGroup(group: GroupItemViewModel) { + val position = itemViewModels.indexOf(group) + if (group.collapsed) { + itemViewModels.removeAll(group.items) + notifyItemRangeRemoved(position + 1, group.items.size) + } else { + itemViewModels.addAll(position + 1, group.items) + setupGroups(group.items.filterIsInstance()) + notifyItemRangeInserted(position + 1, group.items.size) + } + } + } class BindableViewHolder(private val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindingAdapters.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindingAdapters.kt index 6e9e2eb270..baff1b7b23 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindingAdapters.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindingAdapters.kt @@ -16,22 +16,26 @@ */ package com.instructure.pandautils.binding -import android.util.Log +import android.graphics.drawable.GradientDrawable import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup +import android.view.accessibility.AccessibilityNodeInfo import android.webkit.JavascriptInterface import android.widget.ImageView +import androidx.annotation.DrawableRes +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat import androidx.databinding.BindingAdapter import androidx.databinding.DataBindingUtil import androidx.databinding.ViewDataBinding import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.bumptech.glide.Glide import com.instructure.pandautils.BR import com.instructure.pandautils.mvvm.ItemViewModel import com.instructure.pandautils.mvvm.ViewState -import com.instructure.pandautils.utils.setCourseImage -import com.instructure.pandautils.utils.setGone -import com.instructure.pandautils.utils.setVisible +import com.instructure.pandautils.utils.* import com.instructure.pandautils.views.CanvasWebView import com.instructure.pandautils.views.EmptyView import java.net.URLDecoder @@ -75,9 +79,10 @@ private fun handleErrorState(emptyView: EmptyView, error: ViewState.Error) { } } -@BindingAdapter("recyclerViewItemViewModels") -fun bindItemViewModels(recyclerView: RecyclerView, itemViewModels: List?) { - val adapter = getOrCreateAdapter(recyclerView) +@BindingAdapter("recyclerViewItemViewModels", "adapter", requireAll = false) +fun bindItemViewModels(recyclerView: RecyclerView, itemViewModels: List?, bindableAdapter: BindableRecyclerViewAdapter?) { + val adapter = bindableAdapter ?: getOrCreateAdapter(recyclerView) + recyclerView.adapter = adapter adapter.updateItems(itemViewModels) } @@ -91,7 +96,6 @@ private fun getOrCreateAdapter(recyclerView: RecyclerView): BindableRecyclerView recyclerView.adapter as BindableRecyclerViewAdapter } else { val bindableRecyclerAdapter = BindableRecyclerViewAdapter() - recyclerView.adapter = bindableRecyclerAdapter bindableRecyclerAdapter } } @@ -117,10 +121,69 @@ private class JSInterface(private val onLtiButtonPressed: OnLtiButtonPressed) { } } -@BindingAdapter(value = ["imageUrl", "overlayColor"], requireAll = true) +@BindingAdapter(value = ["imageUrl", "overlayColor"], requireAll = false) fun bindImageWithOverlay(imageView: ImageView, imageUrl: String?, overlayColor: Int?) { if (overlayColor != null) { - imageView.setCourseImage(imageUrl, overlayColor, true) + imageView.post { + imageView.setCourseImage(imageUrl, overlayColor, true) + } + } else { + Glide.with(imageView) + .load(imageUrl) + .into(imageView) + } +} + +@BindingAdapter(value = ["borderColor", "borderWidth", "backgroundColor", "borderCornerRadius"], requireAll = false) +fun addBorderToContainer(view: View, borderColor: Int?, borderWidth: Int?, backgroundColor: Int?, borderCornerRadius: Int?) { + val border = GradientDrawable() + val background = backgroundColor ?: 0xffffff + val strokeColor = borderColor + ?: 0x000000 + border.setColor(background) + border.setStroke(borderWidth?.toPx ?: 2.toPx, strokeColor) + border.cornerRadius = borderCornerRadius?.toPx?.toFloat() ?: 4.toPx.toFloat() + view.background = border +} +@BindingAdapter("layout_constraintWidth_percent") +fun bindConstraintWidthPercentage(view: View, percentage: Float) { + val params = view.layoutParams as ConstraintLayout.LayoutParams + params.matchConstraintPercentWidth = percentage + view.layoutParams = params +} + +@BindingAdapter("imageRes") +fun bindImageResource(imageView: ImageView, @DrawableRes imageRes: Int) { + imageView.setImageDrawable(ContextCompat.getDrawable(imageView.context, imageRes)) +} + + +@BindingAdapter("accessibilityClickDescription") +fun bindAccesibilityDelegate(view: View, clickDescription: String) { + view.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo?) { + super.onInitializeAccessibilityNodeInfo(host, info) + info?.addAction(AccessibilityNodeInfo.AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, clickDescription)) + } } } +@BindingAdapter("android:layout_marginBottom") +fun setBottomMargin(view: View, bottomMargin: Int) { + val layoutParams: ViewGroup.MarginLayoutParams = view.layoutParams as ViewGroup.MarginLayoutParams + layoutParams.setMargins(layoutParams.leftMargin, layoutParams.topMargin, + layoutParams.rightMargin, bottomMargin) + view.layoutParams = layoutParams +} + +@BindingAdapter(value = ["userAvatar", "userName"], requireAll = true) +fun bindUserAvatar(imageView: ImageView, userAvatarUrl: String?, userName: String?) { + ProfileUtils.loadAvatarForUser(imageView, userName, userAvatarUrl) +} + +@BindingAdapter("accessibleTouchTarget") +fun bindAccessibleTouchTarget(view: View, accessibleTouchTarget: Boolean?) { + if (accessibleTouchTarget == true) { + view.accessibleTouchTarget() + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/binding/GroupItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/binding/GroupItemViewModel.kt new file mode 100644 index 0000000000..4aea5325ab --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/binding/GroupItemViewModel.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.binding + +import androidx.databinding.BaseObservable +import androidx.databinding.Bindable +import com.instructure.pandautils.BR +import com.instructure.pandautils.mvvm.ItemViewModel + +abstract class GroupItemViewModel( + val collapsable: Boolean, + @get:Bindable var collapsed: Boolean = collapsable, + val items: List +) : ItemViewModel, BaseObservable() { + + open fun toggleItems() { + collapsed = !collapsed + notifyPropertyChanged(BR.collapsed) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt index 340fb0f8d0..693a6693e2 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt @@ -19,15 +19,9 @@ package com.instructure.pandautils.di import android.app.NotificationManager import android.content.Context import android.content.res.Resources -import com.google.android.play.core.appupdate.AppUpdateManager -import com.google.android.play.core.appupdate.AppUpdateManagerFactory -import com.google.android.play.core.appupdate.testing.FakeAppUpdateManager import com.google.firebase.crashlytics.FirebaseCrashlytics import com.instructure.canvasapi2.managers.OAuthManager import com.instructure.pandautils.typeface.TypefaceBehavior -import com.instructure.pandautils.update.UpdateManager -import com.instructure.pandautils.update.UpdatePrefs -import com.instructure.pandautils.utils.ColorApiHelper import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.HtmlContentFormatter import dagger.Module diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/DateTimeModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/DateTimeModule.kt new file mode 100644 index 0000000000..4fc2583c80 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/DateTimeModule.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.di + +import com.instructure.pandautils.utils.date.DateTimeProvider +import com.instructure.pandautils.utils.date.RealDateTimeProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class DateTimeModule { + + @Provides + @Singleton + fun provideDateTimeProvider(): DateTimeProvider { + return RealDateTimeProvider() + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/elementary/ScheduleViewModelModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/elementary/ScheduleViewModelModule.kt new file mode 100644 index 0000000000..371895ad22 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/elementary/ScheduleViewModelModule.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.di.elementary + +import com.instructure.pandautils.utils.MissingItemsPrefs +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +class ScheduleViewModelModule { + + @Provides + fun provideMissingItemsPrefs(): MissingItemsPrefs { + return MissingItemsPrefs + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/ElementaryDashboardPagerAdapter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/ElementaryDashboardPagerAdapter.kt index bd2582df62..17a811b23d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/ElementaryDashboardPagerAdapter.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/ElementaryDashboardPagerAdapter.kt @@ -19,24 +19,13 @@ package com.instructure.pandautils.features.elementary import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter -import com.instructure.canvasapi2.models.CanvasContext -import com.instructure.pandautils.features.elementary.grades.GradesFragment import com.instructure.pandautils.features.elementary.homeroom.HomeroomFragment -import com.instructure.pandautils.features.elementary.resources.ResourcesFragment -import com.instructure.pandautils.features.elementary.schedule.ScheduleFragment class ElementaryDashboardPagerAdapter( - private val canvasContext: CanvasContext, + private val fragments: List, fragmentManager: FragmentManager ) : FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { - private val fragments = listOf( - HomeroomFragment.newInstance(), - ScheduleFragment.newInstance(), - GradesFragment.newInstance(), - ResourcesFragment.newInstance() - ) - override fun getItem(position: Int): Fragment { return fragments[position] } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/GradesFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/GradesFragment.kt index 7dca6cd3bb..1d94566197 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/GradesFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/GradesFragment.kt @@ -16,17 +16,67 @@ */ package com.instructure.pandautils.features.elementary.grades +import android.app.AlertDialog +import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import com.instructure.pandautils.R +import com.instructure.pandautils.databinding.FragmentGradesBinding +import com.instructure.pandautils.utils.toast +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +@AndroidEntryPoint class GradesFragment : Fragment() { + @Inject + lateinit var gradesRouter: GradesRouter + + private val viewModel: GradesViewModel by viewModels() + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return layoutInflater.inflate(R.layout.fragment_grades, container, false) + val binding = FragmentGradesBinding.inflate(inflater, container, false) + binding.lifecycleOwner = this + binding.viewModel = viewModel + + viewModel.events.observe(viewLifecycleOwner, { event -> + event.getContentIfNotHandled()?.let { + handleAction(it) + } + }) + + return binding.root + } + + private fun handleAction(action: GradesAction) { + when (action) { + is GradesAction.OpenCourseGrades -> gradesRouter.openCourseGrades(action.course) + is GradesAction.OpenGradingPeriodsDialog -> showGradingPeriodsDialog(action) + GradesAction.ShowGradingPeriodError -> toast(R.string.failedToLoadGradesForGradingPeriod) + GradesAction.ShowRefreshError -> toast(R.string.failedToRefreshGrades) + } + } + + private fun showGradingPeriodsDialog(action: GradesAction.OpenGradingPeriodsDialog) { + val gradingPeriodNames = action.gradingPeriods + .map { it.name } + .toTypedArray() + + AlertDialog.Builder(context, R.style.AccentDialogTheme) + .setTitle(R.string.selectGradingPeriod) + .setSingleChoiceItems(gradingPeriodNames, action.selectedGradingPeriodIndex) { dialog, which -> sortOrderSelected(dialog, which, action.gradingPeriods) } + .setNegativeButton(R.string.sortByDialogCancel) { dialog, _ -> dialog.dismiss() } + .show() + } + + private fun sortOrderSelected(dialog: DialogInterface?, index: Int, gradingPeriods: List) { + dialog?.dismiss() + val selectedGradingPeriod = gradingPeriods[index] + viewModel.gradingPeriodSelected(selectedGradingPeriod) } companion object { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/GradesRouter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/GradesRouter.kt new file mode 100644 index 0000000000..64947f23a6 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/GradesRouter.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.elementary.grades + +import com.instructure.canvasapi2.models.Course + +interface GradesRouter { + + fun openCourseGrades(course: Course) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/GradesViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/GradesViewData.kt new file mode 100644 index 0000000000..f52acbedff --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/GradesViewData.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.elementary.grades + +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.mvvm.ItemViewModel + +data class GradesViewData(val items: List) + +data class GradingPeriod(val id: Long, val name: String) + +data class GradeRowViewData( + val courseId: Long, + val courseName: String, + val courseColor: String, + val courseImageUrl: String, + val score: Double?, + val gradeText: String) + +sealed class GradesAction { + data class OpenCourseGrades(val course: Course) : GradesAction() + data class OpenGradingPeriodsDialog(val gradingPeriods: List, val selectedGradingPeriodIndex: Int) : GradesAction() + object ShowGradingPeriodError : GradesAction() + object ShowRefreshError : GradesAction() +} + +enum class GradesItemViewType(val viewType: Int) { + GRADING_PERIOD_SELECTOR(0), + GRADE_ROW(1) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/GradesViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/GradesViewModel.kt new file mode 100644 index 0000000000..afb6cd97b9 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/GradesViewModel.kt @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.elementary.grades + +import android.content.res.Resources +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.instructure.canvasapi2.managers.CourseManager +import com.instructure.canvasapi2.managers.EnrollmentManager +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.grades.itemviewmodels.GradeRowItemViewModel +import com.instructure.pandautils.features.elementary.grades.itemviewmodels.GradingPeriodSelectorItemViewModel +import com.instructure.pandautils.mvvm.Event +import com.instructure.pandautils.mvvm.ItemViewModel +import com.instructure.pandautils.mvvm.ViewState +import com.instructure.pandautils.utils.ColorApiHelper +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.math.roundToInt + +private const val CURRENT_GRADING_PERIOD_ID = -1L + +@HiltViewModel +class GradesViewModel @Inject constructor( + private val courseManager: CourseManager, + private val resources: Resources, + private val enrollmentManager: EnrollmentManager +) : ViewModel() { + + + val state: LiveData + get() = _state + private val _state = MutableLiveData() + + val data: LiveData + get() = _data + private val _data = MutableLiveData(GradesViewData(emptyList())) + + val events: LiveData> + get() = _events + private val _events = MutableLiveData>() + + private var gradingPeriodsViewModel: GradingPeriodSelectorItemViewModel? = null + private var courses = emptyList() + + init { + loadInitialData() + } + + private fun loadInitialData() { + _state.postValue(ViewState.Loading) + loadData(false) + } + + private fun loadData(forceNetwork: Boolean, previouslySelectedGradingPeriod: GradingPeriod? = null) { + viewModelScope.launch { + try { + val coursesWithGrades = courseManager.getCoursesWithGradesAsync(forceNetwork).await().dataOrThrow + courses = coursesWithGrades + .filter { !it.homeroomCourse } + + gradingPeriodsViewModel = createGradingPeriodsViewModel(courses) + val gradeRowViewModels = createGradeRowViewModels(courses) + val viewData = createViewData(gradeRowViewModels) + + _data.postValue(viewData) + if (viewData.items.isEmpty()) { + _state.postValue(ViewState.Empty(emptyTitle = R.string.noGradesToDisplay)) + } else { + _state.postValue(ViewState.Success) + } + } catch (e: Exception) { + if (_data.value == null || _data.value?.items?.isNullOrEmpty() == true) { + _state.postValue(ViewState.Error(resources.getString(R.string.failedToLoadGrades))) + } else { + _state.postValue(ViewState.Error()) + _events.postValue(Event(GradesAction.ShowRefreshError)) + } + + if (previouslySelectedGradingPeriod != null) { + gradingPeriodsViewModel?.selectedGradingPeriod = previouslySelectedGradingPeriod + gradingPeriodsViewModel?.notifyChange() + } + } + } + } + + private fun createGradingPeriodsViewModel(courses: List): GradingPeriodSelectorItemViewModel? { + val gradingPeriods = courses + .flatMap { it.gradingPeriods ?: emptyList() } + .distinctBy { gradingPeriod -> gradingPeriod.id } + .map { gradingPeriod -> GradingPeriod(gradingPeriod.id, gradingPeriod.title ?: "") } + + return if (gradingPeriods.isNotEmpty()) { + val currentGradingPeriod = GradingPeriod(CURRENT_GRADING_PERIOD_ID, resources.getString(R.string.currentGradingPeriod)) + val allGradingPeriods = listOf(currentGradingPeriod).plus(gradingPeriods) + GradingPeriodSelectorItemViewModel(allGradingPeriods, currentGradingPeriod, resources) + { index -> _events.postValue(Event(GradesAction.OpenGradingPeriodsDialog(allGradingPeriods, index))) } + } else { + null + } + } + + private fun createGradeRowViewModels(courses: List): List { + return courses + .map { + val enrollment = it.enrollments?.first() + GradeRowItemViewModel(resources, + GradeRowViewData( + it.id, + it.name, + getCourseColor(it), + it.imageUrl ?: "", + if (it.hideFinalGrades) 0.0 else enrollment?.computedCurrentScore, + createGradeText(enrollment?.computedCurrentScore, enrollment?.computedCurrentGrade, it.hideFinalGrades, enrollment?.currentGradingPeriodId ?: 0L != 0L)) + ) { gradeRowClicked(it) } + } + } + + private fun createViewData(gradeRowItems: List): GradesViewData { + val items = mutableListOf() + + if (gradingPeriodsViewModel != null && gradingPeriodsViewModel!!.isNotEmpty() && gradeRowItems.isNotEmpty()) { + items.add(gradingPeriodsViewModel!!) + } + items.addAll(gradeRowItems) + + return GradesViewData(items) + } + + private fun createGradeText(score: Double?, grade: String?, hideFinalGrades: Boolean, notGraded: Boolean = true): String { + return when { + hideFinalGrades -> "--" + !grade.isNullOrEmpty() -> grade + else -> { + val currentScoreRounded = score?.roundToInt() + when { + currentScoreRounded != null -> "$currentScoreRounded%" + notGraded -> resources.getString(R.string.notGraded) + else -> "--" + } + } + } + } + + private fun getCourseColor(course: Course): String { + return if (course.courseColor.isNullOrEmpty()) { + ColorApiHelper.K5_DEFAULT_COLOR + } else { + course.courseColor!! + } + } + + private fun gradeRowClicked(course: Course) { + _events.postValue(Event(GradesAction.OpenCourseGrades(course))) + } + + fun refresh() { + _state.postValue(ViewState.Refresh) + val gradingPeriodId = gradingPeriodsViewModel?.selectedGradingPeriod?.id ?: CURRENT_GRADING_PERIOD_ID + if (gradingPeriodId == CURRENT_GRADING_PERIOD_ID) { + loadData(true) + } else { + loadDataForGradingPeriod(gradingPeriodId, null, true) + } + } + + fun gradingPeriodSelected(gradingPeriod: GradingPeriod) { + if (gradingPeriodsViewModel?.selectedGradingPeriod != gradingPeriod) { + val previouslySelectedGradingPeriod = gradingPeriodsViewModel?.selectedGradingPeriod + gradingPeriodsViewModel?.selectedGradingPeriod = gradingPeriod + gradingPeriodsViewModel?.notifyChange() + + _state.postValue(ViewState.Refresh) + if (gradingPeriod.id == CURRENT_GRADING_PERIOD_ID) { + loadData(false, previouslySelectedGradingPeriod) + } else { + loadDataForGradingPeriod(gradingPeriod.id, previouslySelectedGradingPeriod, false) + } + } + } + + private fun loadDataForGradingPeriod(id: Long, previouslySelectedGradingPeriod: GradingPeriod?, forceNetwork: Boolean) { + viewModelScope.launch { + try { + val enrollments = enrollmentManager.getEnrollmentsForGradingPeriodAsync(id, forceNetwork).await().dataOrThrow + + val gradeRowItems = createGradeRowsForGradingPeriod(enrollments) + val viewData = createViewData(gradeRowItems) + + _state.postValue(ViewState.Success) + _data.postValue(viewData) + } catch (e: Exception) { + _state.postValue(ViewState.Error()) + _events.postValue(Event(GradesAction.ShowGradingPeriodError)) + + if (previouslySelectedGradingPeriod != null) { + gradingPeriodsViewModel?.selectedGradingPeriod = previouslySelectedGradingPeriod + gradingPeriodsViewModel?.notifyChange() + } + } + } + } + + private fun createGradeRowsForGradingPeriod(enrollments: List): List { + val enrollmentsMap = enrollments.associateBy { it.courseId } + return courses + .map { createGradeRowFromEnrollment(it, enrollmentsMap[it.id]) } + } + + private fun createGradeRowFromEnrollment(course: Course, enrollment: Enrollment?): GradeRowItemViewModel { + val gradeRowViewData = GradeRowViewData( + course.id, + course.name, + getCourseColor(course), + course.imageUrl ?: "", + enrollment?.grades?.currentScore, + createGradeText(enrollment?.grades?.currentScore, enrollment?.grades?.currentGrade, course.hideFinalGrades)) + + return GradeRowItemViewModel(resources, gradeRowViewData) { gradeRowClicked(course) } + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/itemviewmodels/GradeRowItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/itemviewmodels/GradeRowItemViewModel.kt new file mode 100644 index 0000000000..017029cffd --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/itemviewmodels/GradeRowItemViewModel.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.elementary.grades.itemviewmodels + +import android.content.res.Resources +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.grades.GradeRowViewData +import com.instructure.pandautils.features.elementary.grades.GradesItemViewType +import com.instructure.pandautils.mvvm.ItemViewModel + +class GradeRowItemViewModel( + private val resources: Resources, + val data: GradeRowViewData, + val onRowClicked: () -> Unit +) : ItemViewModel { + + override val layoutId: Int = R.layout.item_grade_row + + override val viewType: Int = GradesItemViewType.GRADE_ROW.viewType + + val gradeContentDescription: String + get() { + return if (data.gradeText == "--") { + resources.getString(R.string.a11y_gradesNotAvailableContentDescription) + } else { + data.gradeText + } + } + + val percentage: Float + get() { + if (data.score == null) return 0.0f + return data.score.toFloat() / 100 + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/itemviewmodels/GradingPeriodSelectorItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/itemviewmodels/GradingPeriodSelectorItemViewModel.kt new file mode 100644 index 0000000000..05174e3fcf --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/grades/itemviewmodels/GradingPeriodSelectorItemViewModel.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.elementary.grades.itemviewmodels + +import android.content.res.Resources +import androidx.databinding.BaseObservable +import androidx.databinding.Bindable +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.grades.GradesItemViewType +import com.instructure.pandautils.features.elementary.grades.GradingPeriod +import com.instructure.pandautils.mvvm.ItemViewModel + +class GradingPeriodSelectorItemViewModel( + private val gradingPeriods: List, + @get:Bindable var selectedGradingPeriod: GradingPeriod, + resources: Resources, + val onItemClick: (Int) -> Unit +) : BaseObservable(), ItemViewModel { + + override val layoutId: Int = R.layout.item_grading_period_selector + + override val viewType: Int = GradesItemViewType.GRADING_PERIOD_SELECTOR.viewType + + val accessibilityContentDescription: String = resources.getString(R.string.a11y_gradingPeriodSelectorClickDescription) + + fun onClick() { + val index = gradingPeriods.indexOfFirst { it.id == selectedGradingPeriod.id } + onItemClick(index) + } + + fun isNotEmpty() = gradingPeriods.isNotEmpty() +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/CourseCardCreator.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/CourseCardCreator.kt index d719f6c5ff..c10ba88059 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/CourseCardCreator.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/CourseCardCreator.kt @@ -21,10 +21,7 @@ import androidx.lifecycle.MutableLiveData import com.instructure.canvasapi2.managers.AnnouncementManager import com.instructure.canvasapi2.managers.PlannerManager import com.instructure.canvasapi2.managers.UserManager -import com.instructure.canvasapi2.models.Assignment -import com.instructure.canvasapi2.models.Course -import com.instructure.canvasapi2.models.DiscussionTopicHeader -import com.instructure.canvasapi2.models.PlannerItem +import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.toApiString import com.instructure.pandautils.R import com.instructure.pandautils.features.elementary.homeroom.itemviewmodels.CourseCardItemViewModel @@ -33,8 +30,6 @@ import com.instructure.pandautils.utils.ColorApiHelper import kotlinx.coroutines.awaitAll import org.threeten.bp.LocalDate -private const val PLANNABLE_TYPE_ASSIGNMENT = "assignment" - class CourseCardCreator( private val plannerManager: PlannerManager, private val userManager: UserManager, @@ -101,7 +96,7 @@ class CourseCardCreator( } private fun isNotSubmittedAssignment(it: PlannerItem) = - it.courseId != null && it.submissionState?.submitted == false && it.plannableType == PLANNABLE_TYPE_ASSIGNMENT && it.submissionState?.missing == false + it.courseId != null && it.submissionState?.submitted == false && it.plannableType == PlannableType.ASSIGNMENT && it.submissionState?.missing == false private fun createDueTextForCourse(dueCount: Int): String { return if (dueCount == 0) { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/HomeroomFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/HomeroomFragment.kt index 7558241985..e043bc4220 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/HomeroomFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/HomeroomFragment.kt @@ -33,6 +33,7 @@ import com.instructure.pandautils.BuildConfig import com.instructure.pandautils.R import com.instructure.pandautils.databinding.FragmentHomeroomBinding import com.instructure.pandautils.discussions.DiscussionUtils +import com.instructure.pandautils.navigation.WebViewRouter import com.instructure.pandautils.utils.children import com.instructure.pandautils.utils.toast import com.instructure.pandautils.views.CanvasWebView @@ -48,6 +49,9 @@ class HomeroomFragment : Fragment() { @Inject lateinit var homeroomRouter: HomeroomRouter + @Inject + lateinit var webViewRouter: WebViewRouter + private val viewModel: HomeroomViewModel by viewModels() private var updateAssignments = false @@ -129,13 +133,13 @@ class HomeroomFragment : Fragment() { announcementWebView.settings.loadWithOverviewMode = true announcementWebView.canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback { override fun routeInternallyCallback(url: String) { - homeroomRouter.routeInternally(url) + webViewRouter.routeInternally(url) } - override fun canRouteInternallyDelegate(url: String): Boolean = homeroomRouter.canRouteInternally(url) + override fun canRouteInternallyDelegate(url: String): Boolean = webViewRouter.canRouteInternally(url) override fun openMediaFromWebView(mime: String, url: String, filename: String) { - homeroomRouter.openMedia(url) + webViewRouter.openMedia(url) } override fun onPageStartedCallback(webView: WebView, url: String) = Unit diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/HomeroomRouter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/HomeroomRouter.kt index b4b1b83bdb..c9de6fdae6 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/HomeroomRouter.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/HomeroomRouter.kt @@ -22,12 +22,6 @@ import com.instructure.canvasapi2.models.DiscussionTopicHeader interface HomeroomRouter { - fun canRouteInternally(url: String): Boolean - - fun routeInternally(url: String) - - fun openMedia(url: String) - fun openAnnouncements(canvasContext: CanvasContext) fun openCourse(course: Course) @@ -37,4 +31,5 @@ interface HomeroomRouter { fun openAnnouncementDetails(course: Course, announcement: DiscussionTopicHeader) fun updateColors() + } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/ResourcesFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/ResourcesFragment.kt index 0be189512b..fe74a91295 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/ResourcesFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/ResourcesFragment.kt @@ -16,17 +16,114 @@ */ package com.instructure.pandautils.features.elementary.resources +import android.app.AlertDialog +import android.content.DialogInterface +import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.webkit.WebView import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.instructure.canvasapi2.models.LTITool +import com.instructure.pandautils.BuildConfig import com.instructure.pandautils.R +import com.instructure.pandautils.databinding.FragmentResourcesBinding +import com.instructure.pandautils.discussions.DiscussionUtils +import com.instructure.pandautils.features.elementary.resources.itemviewmodels.ResourcesRouter +import com.instructure.pandautils.navigation.WebViewRouter +import com.instructure.pandautils.utils.children +import com.instructure.pandautils.utils.toast +import com.instructure.pandautils.views.CanvasWebView +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.android.synthetic.main.fragment_resources.* +import kotlinx.android.synthetic.main.item_important_links.view.* +import javax.inject.Inject +@AndroidEntryPoint class ResourcesFragment : Fragment() { - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return layoutInflater.inflate(R.layout.fragment_resources, container, false) + @Inject + lateinit var resourcesRouter: ResourcesRouter + + @Inject + lateinit var webViewRouter: WebViewRouter + + private val viewModel: ResourcesViewModel by viewModels() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val binding = FragmentResourcesBinding.inflate(inflater, container, false) + binding.lifecycleOwner = this + binding.viewModel = viewModel + + viewModel.events.observe(viewLifecycleOwner, { event -> + event.getContentIfNotHandled()?.let { + handleAction(it) + } + }) + + return binding.root + } + + private fun handleAction(action: ResourcesAction) { + when (action) { + is ResourcesAction.OpenLtiApp -> showCourseSelectorDialog(action.ltiTools) + is ResourcesAction.OpenComposeMessage -> resourcesRouter.openComposeMessage(action.recipient) + ResourcesAction.ImportantLinksViewsReady -> setupWebViews() + ResourcesAction.ShowRefreshError -> toast(R.string.failedToRefreshResources) + is ResourcesAction.WebLtiButtonPressed -> DiscussionUtils.launchIntent(requireContext(), action.url) + } + } + + private fun showCourseSelectorDialog(ltiTools: List) { + val dialogEntries = ltiTools + .map { it.contextName } + .toTypedArray() + + AlertDialog.Builder(context, R.style.AccentDialogTheme) + .setTitle(R.string.chooseACourse) + .setItems(dialogEntries) { dialog, which -> openSelectedLti(dialog, which, ltiTools) } + .setNegativeButton(R.string.sortByDialogCancel) { dialog, _ -> dialog.dismiss() } + .show() + } + + private fun openSelectedLti(dialog: DialogInterface?, index: Int, ltiTools: List) { + dialog?.dismiss() + val ltiTool = ltiTools[index] + resourcesRouter.openLti(ltiTool) + } + + private fun setupWebViews() { + importantLinksContainer.children.forEach { + val webView = it.importantLinksWebView + if (webView != null) { + setupWebView(webView) + } + } + } + + private fun setupWebView(webView: CanvasWebView) { + WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG) + webView.setBackgroundColor(Color.WHITE) + webView.settings.allowFileAccess = true + webView.settings.loadWithOverviewMode = true + webView.canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback { + override fun routeInternallyCallback(url: String) { + webViewRouter.routeInternally(url) + } + + override fun canRouteInternallyDelegate(url: String): Boolean = webViewRouter.canRouteInternally(url) + + override fun openMediaFromWebView(mime: String, url: String, filename: String) { + webViewRouter.openMedia(url) + } + + override fun onPageStartedCallback(webView: WebView, url: String) = Unit + override fun onPageFinishedCallback(webView: WebView, url: String) = Unit + } + + webView.addVideoClient(requireActivity()) } companion object { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/ResourcesViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/ResourcesViewData.kt new file mode 100644 index 0000000000..71663ec7e9 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/ResourcesViewData.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.elementary.resources + +import com.instructure.canvasapi2.models.LTITool +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.mvvm.ItemViewModel + +data class ResourcesViewData(val importantLinksItems: List, val actionItems: List) { + fun isEmpty() = importantLinksItems.isEmpty() && actionItems.isEmpty() +} + +data class ResourcesHeaderViewData(val title: String, val hasDivider: Boolean = false) + +data class LtiApplicationViewData(val title: String, val imageUrl: String, val ltiUrl: String) + +data class ContactInfoViewData(val name: String, val description: String, val imageUrl: String) + +data class ImportantLinksViewData(val courseName: String, val htmlContent: String, val hasDivider: Boolean = false) + +sealed class ResourcesAction { + data class OpenLtiApp(val ltiTools: List) : ResourcesAction() + data class OpenComposeMessage(val recipient: User) : ResourcesAction() + object ImportantLinksViewsReady : ResourcesAction() + data class WebLtiButtonPressed(val url: String) : ResourcesAction() + object ShowRefreshError : ResourcesAction() +} + +enum class ResourcesItemViewType(val viewType: Int) { + RESOURCES_HEADER(0), + LTI_APPLICATION(1), + CONTACT_INFO(2) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/ResourcesViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/ResourcesViewModel.kt new file mode 100644 index 0000000000..0d5c381dc2 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/ResourcesViewModel.kt @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.elementary.resources + +import android.content.res.Resources +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.instructure.canvasapi2.managers.CourseManager +import com.instructure.canvasapi2.managers.ExternalToolManager +import com.instructure.canvasapi2.managers.OAuthManager +import com.instructure.canvasapi2.managers.UserManager +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.models.LTITool +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.resources.itemviewmodels.ContactInfoItemViewModel +import com.instructure.pandautils.features.elementary.resources.itemviewmodels.ImportantLinksItemViewModel +import com.instructure.pandautils.features.elementary.resources.itemviewmodels.LtiApplicationItemViewModel +import com.instructure.pandautils.features.elementary.resources.itemviewmodels.ResourcesHeaderViewModel +import com.instructure.pandautils.mvvm.Event +import com.instructure.pandautils.mvvm.ItemViewModel +import com.instructure.pandautils.mvvm.ViewState +import com.instructure.pandautils.utils.HtmlContentFormatter +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.launch +import java.util.regex.Pattern +import javax.inject.Inject + +@HiltViewModel +class ResourcesViewModel @Inject constructor( + private val resources: Resources, + private val courseManager: CourseManager, + private val userManager: UserManager, + private val externalToolManager: ExternalToolManager, + private val oAuthManager: OAuthManager, + private val htmlContentFormatter: HtmlContentFormatter +) : ViewModel() { + + val state: LiveData + get() = _state + private val _state = MutableLiveData() + + val data: LiveData + get() = _data + private val _data = MutableLiveData(ResourcesViewData(emptyList(), emptyList())) + + val events: LiveData> + get() = _events + private val _events = MutableLiveData>() + + init { + _state.postValue(ViewState.Loading) + loadData(false) + } + + private fun loadData(forceNetwork: Boolean) { + viewModelScope.launch { + try { + val coursesResult = courseManager.getCoursesWithSyllabusAsyncWithActiveEnrollmentAsync(forceNetwork).await() + + val courses = coursesResult.dataOrThrow + .filter { !it.homeroomCourse } + + val homeroomCourses = coursesResult.dataOrThrow.filter { it.homeroomCourse } + val importantLinks = createImportantLinks(homeroomCourses) + + val actionItems = createActionItems(courses, homeroomCourses, forceNetwork) + + val viewData = ResourcesViewData(importantLinks, actionItems) + _data.postValue(viewData) + if (viewData.isEmpty()) { + _state.postValue(ViewState.Empty(emptyTitle = R.string.resourcesEmptyMessage)) + } else { + _state.postValue(ViewState.Success) + } + } catch (e: Exception) { + if (_data.value == null || _data.value?.isEmpty() == true) { + _state.postValue(ViewState.Error(resources.getString(R.string.failedToLoadResources))) + } else { + _state.postValue(ViewState.Error()) + _events.postValue(Event(ResourcesAction.ShowRefreshError)) + } + } + } + } + + private fun createImportantLinks(homeroomCourses: List): List { + val homeroomCoursesWithSyllabus = homeroomCourses.filter { !it.syllabusBody.isNullOrEmpty() } + return if (homeroomCoursesWithSyllabus.size > 1) { + homeroomCoursesWithSyllabus + .mapIndexed { index, course -> ImportantLinksItemViewModel( + ImportantLinksViewData(course.name, course.syllabusBody ?: "", index < homeroomCoursesWithSyllabus.size - 1), + ::ltiButtonPressed) } + } else { + homeroomCoursesWithSyllabus + .map { ImportantLinksItemViewModel(ImportantLinksViewData("", it.syllabusBody ?: ""), ::ltiButtonPressed) } + } + } + + private fun ltiButtonPressed(html: String, htmlContent: String) { + viewModelScope.launch { + try { + val matcher = Pattern.compile("src=\"([^\"]+)\"").matcher(htmlContent) + matcher.find() + val url = matcher.group(1) + + if (url == null) { + _events.postValue(Event(ResourcesAction.WebLtiButtonPressed(html))) + return@launch + } + + // Get an authenticated session so the user doesn't have to log in + val authenticatedSessionURL = oAuthManager.getAuthenticatedSessionAsync(url).await().dataOrThrow.sessionUrl + val newUrl = htmlContentFormatter.createAuthenticatedLtiUrl(html, authenticatedSessionURL) + + _events.postValue(Event(ResourcesAction.WebLtiButtonPressed(newUrl))) + } catch (e: Exception) { + // Couldn't get the authenticated session, try to load it without it + _events.postValue(Event(ResourcesAction.WebLtiButtonPressed(html))) + } + } + } + + private suspend fun createActionItems(courses: List, homeroomCourses: List, forceNetwork: Boolean): List { + val actionItems = mutableListOf() + + val ltiApps = if (courses.isNotEmpty()) { + createLtiApps(courses, forceNetwork) + } else { + emptyList() + } + if (ltiApps.isNotEmpty()) { + actionItems.add(ResourcesHeaderViewModel(ResourcesHeaderViewData(resources.getString(R.string.studentApplications)))) + actionItems.addAll(ltiApps) + } + + val staffInfo = createStaffInfo(homeroomCourses, forceNetwork) + if (staffInfo.isNotEmpty()) { + actionItems.add(ResourcesHeaderViewModel(ResourcesHeaderViewData(resources.getString(R.string.staffContactInfo), true))) + actionItems.addAll(staffInfo) + } + + return actionItems + } + + private suspend fun createLtiApps(courses: List, forceNetwork: Boolean): List { + val contextIds = courses + .map { it.contextId } + + val ltiTools = externalToolManager.getExternalToolsForCoursesAsync(contextIds, forceNetwork).await().dataOrNull + + val ltiToolsMapById = mutableMapOf>() + ltiTools?.forEach { + if (!ltiToolsMapById.contains(it.id)) { + ltiToolsMapById[it.id] = mutableListOf() + } + ltiToolsMapById[it.id]?.add(it) + } + + val displayedLtiTools = ltiTools?.distinctBy { it.id } + + return displayedLtiTools + ?.mapIndexed { i: Int, ltiTool: LTITool -> + createLtiApplicationItem(ltiTool, i == displayedLtiTools.size - 1, ltiToolsMapById[ltiTool.id] ?: emptyList()) } + ?: emptyList() + } + + private fun createLtiApplicationItem(ltiTool: LTITool, isLast: Boolean, courseLtiTools: List): LtiApplicationItemViewModel { + val margin = if (isLast) resources.getDimension(R.dimen.ltiAppsBottomMargin).toInt() else 0 + return LtiApplicationItemViewModel( + LtiApplicationViewData( + ltiTool.courseNavigation?.text ?: ltiTool.name ?: "", + ltiTool.iconUrl ?: "", + ltiTool.url ?: ""), + margin + ) { _events.postValue(Event(ResourcesAction.OpenLtiApp(courseLtiTools))) } + } + + private suspend fun createStaffInfo(homeroomCourses: List, forceNetwork: Boolean): List { + val teachers = homeroomCourses + .map { userManager.getTeacherListForCourseAsync(it.id, forceNetwork) } + .awaitAll() + .map { result -> result.dataOrNull ?: emptyList() } + .flatten() + .distinctBy { user: User -> user.id } + + return teachers.map { + ContactInfoItemViewModel(ContactInfoViewData(it.shortName ?: "", getRoleString(it.enrollments[0].role), it.avatarUrl ?: "")) { + _events.postValue(Event(ResourcesAction.OpenComposeMessage(it))) + } + } + } + + private fun getRoleString(role: Enrollment.EnrollmentType?): String { + return when (role) { + Enrollment.EnrollmentType.Teacher -> resources.getString(R.string.staffRoleTeacher) + Enrollment.EnrollmentType.Ta -> resources.getString(R.string.staffRoleTeacherAssistant) + else -> "" + } + } + + fun refresh() { + _state.postValue(ViewState.Refresh) + loadData(true) + } + + fun onImportantLinksViewsReady() { + _events.postValue(Event(ResourcesAction.ImportantLinksViewsReady)) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/itemviewmodels/ContactInfoItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/itemviewmodels/ContactInfoItemViewModel.kt new file mode 100644 index 0000000000..137a3f2ff3 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/itemviewmodels/ContactInfoItemViewModel.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.elementary.resources.itemviewmodels + +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.resources.ContactInfoViewData +import com.instructure.pandautils.features.elementary.resources.ResourcesItemViewType +import com.instructure.pandautils.mvvm.ItemViewModel + +class ContactInfoItemViewModel( + val data: ContactInfoViewData, + val onClick: () -> Unit +) : ItemViewModel { + + override val layoutId: Int = R.layout.item_contact_info + + override val viewType: Int = ResourcesItemViewType.CONTACT_INFO.viewType +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/itemviewmodels/ImportantLinksItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/itemviewmodels/ImportantLinksItemViewModel.kt new file mode 100644 index 0000000000..841a6e3779 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/itemviewmodels/ImportantLinksItemViewModel.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.elementary.resources.itemviewmodels + +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.resources.ImportantLinksViewData +import com.instructure.pandautils.mvvm.ItemViewModel + +class ImportantLinksItemViewModel( + val data: ImportantLinksViewData, + private val onLtiButtonPressed: (url: String, content: String) -> Unit +) : ItemViewModel { + + override val layoutId: Int = R.layout.item_important_links + + fun onLtiButtonPressed(url: String) { + onLtiButtonPressed(url, data.htmlContent) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/itemviewmodels/LtiApplicationItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/itemviewmodels/LtiApplicationItemViewModel.kt new file mode 100644 index 0000000000..17c0912e83 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/itemviewmodels/LtiApplicationItemViewModel.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.elementary.resources.itemviewmodels + +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.resources.LtiApplicationViewData +import com.instructure.pandautils.features.elementary.resources.ResourcesItemViewType +import com.instructure.pandautils.mvvm.ItemViewModel + +class LtiApplicationItemViewModel( + val data: LtiApplicationViewData, + val marginBottom: Int, + val onClick: () -> Unit) : ItemViewModel { + + override val layoutId: Int = R.layout.item_lti_application + + override val viewType: Int = ResourcesItemViewType.LTI_APPLICATION.viewType +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/itemviewmodels/ResourcesHeaderViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/itemviewmodels/ResourcesHeaderViewModel.kt new file mode 100644 index 0000000000..efeb42a983 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/itemviewmodels/ResourcesHeaderViewModel.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.elementary.resources.itemviewmodels + +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.resources.ResourcesHeaderViewData +import com.instructure.pandautils.features.elementary.resources.ResourcesItemViewType +import com.instructure.pandautils.mvvm.ItemViewModel + +class ResourcesHeaderViewModel(val data: ResourcesHeaderViewData) : ItemViewModel { + + override val layoutId: Int = R.layout.item_resources_header + + override val viewType: Int = ResourcesItemViewType.RESOURCES_HEADER.viewType +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/itemviewmodels/ResourcesRouter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/itemviewmodels/ResourcesRouter.kt new file mode 100644 index 0000000000..678934297f --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/itemviewmodels/ResourcesRouter.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.elementary.resources.itemviewmodels + +import com.instructure.canvasapi2.models.LTITool +import com.instructure.canvasapi2.models.User + +interface ResourcesRouter { + fun openLti(ltiTool: LTITool) + fun openComposeMessage(user: User) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/ScheduleFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/ScheduleFragment.kt index 486ffa1d17..f536821b61 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/ScheduleFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/ScheduleFragment.kt @@ -21,17 +21,116 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import com.instructure.pandautils.R +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.instructure.pandautils.databinding.FragmentScheduleBinding +import com.instructure.pandautils.features.elementary.schedule.pager.SchedulePagerFragment +import com.instructure.pandautils.utils.StringArg +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.android.synthetic.main.fragment_schedule.* +import javax.inject.Inject +@AndroidEntryPoint class ScheduleFragment : Fragment() { - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return layoutInflater.inflate(R.layout.fragment_schedule, container, false) + @Inject + lateinit var scheduleRouter: ScheduleRouter + + private val viewModel: ScheduleViewModel by viewModels() + + private val adapter = ScheduleRecyclerViewAdapter() + + private var startDateString by StringArg() + + private var recyclerView: RecyclerView? = null + + private val onScrollListener = object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + checkFirstPosition() + } } - companion object { - fun newInstance(): ScheduleFragment { - return ScheduleFragment() + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val binding = FragmentScheduleBinding.inflate(layoutInflater, container, false) + binding.lifecycleOwner = this + binding.viewModel = viewModel + binding.adapter = adapter + recyclerView = binding.scheduleRecyclerView + + viewModel.getDataForDate(startDateString) + + viewModel.events.observe(viewLifecycleOwner, { event -> + event.getContentIfNotHandled()?.let { + handleAction(it) + } + }) + + return binding.root + } + + override fun onResume() { + super.onResume() + recyclerView?.addOnScrollListener(onScrollListener) + } + + override fun onPause() { + super.onPause() + recyclerView?.removeOnScrollListener(onScrollListener) + } + + private fun handleAction(action: ScheduleAction) { + when (action) { + is ScheduleAction.OpenCourse -> scheduleRouter.openCourse(action.course) + is ScheduleAction.OpenAssignment -> scheduleRouter.openAssignment(action.canvasContext, action.assignmentId) + is ScheduleAction.OpenCalendarEvent -> scheduleRouter.openCalendarEvent( + action.canvasContext, + action.scheduleItemId + ) + is ScheduleAction.OpenQuiz -> { + scheduleRouter.openQuiz(action.canvasContext, action.htmlUrl) + } + is ScheduleAction.OpenDiscussion -> { + scheduleRouter.openDiscussion(action.canvasContext, action.id, action.title) + } + is ScheduleAction.JumpToToday -> { + jumpToToday() + } } } + + fun jumpToToday() { + if (recyclerView?.layoutManager is LinearLayoutManager) { + (recyclerView?.layoutManager as LinearLayoutManager).scrollToPositionWithOffset( + viewModel.todayPosition, + 0 + ) + } + } + + override fun setMenuVisibility(menuVisible: Boolean) { + if (menuVisible) { + checkFirstPosition() + } + super.setMenuVisibility(menuVisible) + } + + fun checkFirstPosition() { + val firstItemPosition = + (scheduleRecyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() + val todayRange = viewModel.getTodayRange() + if (todayRange != null) { + toggleJumpToTodayButton(firstItemPosition !in todayRange) + } + } + + private fun toggleJumpToTodayButton(visible: Boolean) { + (requireParentFragment() as SchedulePagerFragment).setTodayButtonVisibility(visible) + } + + companion object { + + fun newInstance(startDate: String) = ScheduleFragment().apply { startDateString = startDate } + } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/ScheduleRecyclerViewAdapter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/ScheduleRecyclerViewAdapter.kt new file mode 100644 index 0000000000..2f02fca717 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/ScheduleRecyclerViewAdapter.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.features.elementary.schedule + +import android.view.LayoutInflater +import androidx.databinding.ViewDataBinding +import androidx.databinding.library.baseAdapters.BR +import androidx.recyclerview.widget.RecyclerView +import com.instructure.pandautils.adapters.itemdecorations.StickyHeaderInterface +import com.instructure.pandautils.adapters.itemdecorations.StickyHeaderItemDecoration +import com.instructure.pandautils.binding.BindableRecyclerViewAdapter +import com.instructure.pandautils.databinding.ItemScheduleDayHeaderBinding +import com.instructure.pandautils.features.elementary.schedule.itemviewmodels.ScheduleDayGroupItemViewModel + +class ScheduleRecyclerViewAdapter : BindableRecyclerViewAdapter(), StickyHeaderInterface { + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + super.onAttachedToRecyclerView(recyclerView) + recyclerView.addItemDecoration(StickyHeaderItemDecoration(this)) + } + + override fun getHeaderPositionForItem(itemPosition: Int): Int { + var startPosition = itemPosition + var headerPosition = 0 + do { + if (isHeader(startPosition)) { + headerPosition = startPosition + break + } + startPosition -= 1 + } while (startPosition >= 0) + return headerPosition + } + + override fun getHeaderBinding( + headerPosition: Int, + parent: RecyclerView, + hasChildInContact: Boolean + ): ViewDataBinding { + val binding = ItemScheduleDayHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) + binding.setVariable(BR.itemViewModel, itemViewModels[headerPosition]) + binding.hasDivider = hasChildInContact + binding.invalidateAll() + return binding + } + + override fun isHeader(itemPosition: Int): Boolean { + return if (itemPosition == RecyclerView.NO_POSITION) { + false + } else { + itemViewModels[itemPosition] is ScheduleDayGroupItemViewModel + } + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/ScheduleRouter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/ScheduleRouter.kt new file mode 100644 index 0000000000..52324f9330 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/ScheduleRouter.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.features.elementary.schedule + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DiscussionTopicHeader + +interface ScheduleRouter { + + fun openAssignment(canvasContext: CanvasContext, assignmentId: Long) + + fun openCalendarEvent(canvasContext: CanvasContext, scheduleItemId: Long) + + fun openAnnouncementDetails(course: Course, announcement: DiscussionTopicHeader) + + fun openQuiz(canvasContext: CanvasContext, htmlUrl: String) + + fun openDiscussion(canvasContext: CanvasContext, discussionId: Long, discussionTitle: String) + + fun openCourse(course: Course) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/ScheduleViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/ScheduleViewData.kt new file mode 100644 index 0000000000..49ea8ca8d8 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/ScheduleViewData.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.features.elementary.schedule + +import androidx.annotation.* +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.schedule.itemviewmodels.ScheduleDayGroupItemViewModel +import com.instructure.pandautils.features.elementary.schedule.itemviewmodels.SchedulePlannerItemTagItemViewModel +import com.instructure.pandautils.features.elementary.schedule.itemviewmodels.SchedulePlannerItemViewModel +import com.instructure.pandautils.mvvm.ItemViewModel + +data class ScheduleViewData(val itemViewModels: List) + +data class ScheduleCourseViewData( + val courseName: String, + val openable: Boolean, + val courseColor: String, + val imageUrl: String, + val plannerItems: List +) + +data class SchedulePlannerItemData( + val title: String, + val type: PlannerItemType, + val points: String?, + val dueDate: String?, + val openable: Boolean, + val contentDescription: String, + val chips: List +) + +data class ScheduleEmptyViewData( + val title: String +) + +data class SchedulePlannerItemTag( + val text: String, + @ColorInt val color: Int +) + +data class ScheduleMissingItemData( + val title: String?, + val dueString: String?, + val points: String?, + val type: PlannerItemType, + val courseName: String?, + val courseColor: String, + val contentDescription: String +) + +enum class PlannerItemType(@DrawableRes val iconRes: Int) { + ANNOUNCEMENT(R.drawable.ic_announcement), + ASSIGNMENT(R.drawable.ic_assignment), + QUIZ(R.drawable.ic_quiz), + DISCUSSION(R.drawable.ic_discussion), + PEER_REVIEW(R.drawable.ic_peer_review), + CALENDAR_EVENT(R.drawable.ic_calendar), + PAGE(R.drawable.ic_pages), + TO_DO(R.drawable.ic_calendar) +} + +enum class ScheduleItemViewModelType(val viewType: Int) { + COURSE(1), + DAY_HEADER(2), + PLANNER_ITEM(3), + EMPTY(4), + MISSING_HEADER(5), + MISSING_ITEM(6) +} + +sealed class PlannerItemTag(val text: Int, @ColorRes val color: Int) { + object Excused: PlannerItemTag(R.string.schedule_tag_excused, R.color.textLightGray) + object Graded : PlannerItemTag(R.string.schedule_tag_graded, R.color.textLightGray) + data class Replies(val replyCount: Int) : PlannerItemTag(R.plurals.schedule_tag_replies, R.color.textLightGray) + object Feedback : PlannerItemTag(R.string.schedule_tag_feedback, R.color.textLightGray) + object Late : PlannerItemTag(R.string.schedule_tag_late, R.color.canvasRed) + object Redo : PlannerItemTag(R.string.schedule_tag_redo, R.color.canvasRed) + object Missing : PlannerItemTag(R.string.schedule_tag_missing, R.color.canvasRed) +} + +sealed class ScheduleAction { + data class OpenCourse(val course: Course) : ScheduleAction() + data class OpenAssignment(val canvasContext: CanvasContext, val assignmentId: Long) : ScheduleAction() + data class OpenCalendarEvent(val canvasContext: CanvasContext, val scheduleItemId: Long) : ScheduleAction() + data class OpenQuiz(val canvasContext: CanvasContext, val htmlUrl: String) : ScheduleAction() + data class OpenDiscussion(val canvasContext: CanvasContext, val id: Long, val title: String) : ScheduleAction() + object JumpToToday : ScheduleAction() +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/ScheduleViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/ScheduleViewModel.kt new file mode 100644 index 0000000000..66002aa8b5 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/ScheduleViewModel.kt @@ -0,0 +1,572 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.features.elementary.schedule + +import android.content.res.Resources +import android.util.Range +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.instructure.canvasapi2.managers.* +import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.toApiString +import com.instructure.canvasapi2.utils.toDate +import com.instructure.pandautils.R +import com.instructure.pandautils.binding.GroupItemViewModel +import com.instructure.pandautils.features.elementary.schedule.itemviewmodels.* +import com.instructure.pandautils.mvvm.Event +import com.instructure.pandautils.mvvm.ItemViewModel +import com.instructure.pandautils.mvvm.ViewState +import com.instructure.pandautils.utils.* +import com.instructure.pandautils.utils.date.DateTimeProvider +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.launch +import java.text.DecimalFormat +import java.text.SimpleDateFormat +import java.util.* +import javax.inject.Inject + +const val TODO_COLOR = "#0081BD" + +@HiltViewModel +class ScheduleViewModel @Inject constructor( + private val apiPrefs: ApiPrefs, + private val resources: Resources, + private val plannerManager: PlannerManager, + private val courseManager: CourseManager, + private val userManager: UserManager, + private val calendarEventManager: CalendarEventManager, + private val assignmentManager: AssignmentManager, + private val missingItemsPrefs: MissingItemsPrefs, + private val dateTimeProvider: DateTimeProvider +) : ViewModel() { + + private lateinit var startDate: Date + + private lateinit var missingSubmissions: List + private lateinit var calendarEvents: Map + private lateinit var discussions: Map + private lateinit var plannerItems: List + private lateinit var coursesMap: Map + + private var todayHeader: ScheduleDayGroupItemViewModel? = null + private val simpleDateFormat = SimpleDateFormat("hh:mm aa", Locale.getDefault()) + + var todayPosition: Int = -1 + + val state: LiveData + get() = _state + private val _state = MutableLiveData() + + val data: LiveData + get() = _data + private val _data = MutableLiveData() + + val events: LiveData> + get() = _events + private val _events = MutableLiveData>() + + fun getDataForDate(dateString: String) { + _state.postValue(ViewState.Loading) + startDate = dateString.toDate() ?: dateTimeProvider.getCalendar().time + getData(false) + } + + fun refresh() { + _state.postValue(ViewState.Refresh) + getData(true) + } + + private fun jumpToToday() { + if (todayPosition != -1) { + _events.postValue(Event(ScheduleAction.JumpToToday)) + } + } + + private fun getData(forceNetwork: Boolean) { + viewModelScope.launch { + try { + val weekStart = startDate.getLastSunday() + + val courses = courseManager.getCoursesAsync(forceNetwork).await() + coursesMap = courses.dataOrThrow + .associateBy { it.id } + + plannerItems = plannerManager.getPlannerItemsAsync( + forceNetwork, + weekStart.toApiString(), + startDate.getNextSaturday().toApiString() + ) + .await() + .dataOrNull + .orEmpty() + + missingSubmissions = + userManager.getAllMissingSubmissionsAsync(forceNetwork).await().dataOrNull.orEmpty() + + calendarEvents = plannerItems.filter { it.plannableType == PlannableType.CALENDAR_EVENT } + .map { calendarEventManager.getCalendarEventAsync(it.plannable.id, forceNetwork) } + .awaitAll() + .map { it.dataOrNull } + .associateBy { it?.id } + + discussions = + plannerItems.filter { (it.plannableType == PlannableType.ANNOUNCEMENT || it.plannableType == PlannableType.DISCUSSION_TOPIC) && it.courseId != null } + .map { assignmentManager.getAssignmentAsync(it.plannable.id, it.courseId!!, forceNetwork) } + .awaitAll() + .map { it.dataOrNull } + .associateBy { it?.id } + + val itemViewModels = mutableListOf() + for (i in 0..6) { + val calendar = dateTimeProvider.getCalendar() + calendar.time = weekStart + calendar.add(Calendar.DATE, i) + val date = calendar.time + + itemViewModels.add(createItemsForDate(date)) + } + _data.postValue(ScheduleViewData(itemViewModels)) + _state.postValue(ViewState.Success) + todayPosition = calculateTodayPosition(itemViewModels) + jumpToToday() + } catch (e: Exception) { + e.printStackTrace() + _state.postValue(ViewState.Error(resources.getString(R.string.schedule_error_message))) + } + } + } + + private fun createItemsForDate(date: Date): ScheduleDayGroupItemViewModel { + val items = mutableListOf() + + items.addAll(createCourseItems(date)) + + val today = dateTimeProvider.getCalendar().time + if (date.isSameDay(today) && missingSubmissions.isNotEmpty()) { + items.add(createMissingItems()) + } + + val dayHeader = createDayHeader(date, items) + + if (date.isSameDay(today)) { + todayHeader = dayHeader + } + + return dayHeader + } + + private fun createMissingItems(): ScheduleMissingItemsGroupItemViewModel { + val missingItems = missingSubmissions.map { assignment -> + ScheduleMissingItemViewModel( + data = ScheduleMissingItemData( + title = assignment.name, + dueString = assignment.dueDate?.let { + resources.getString( + R.string.schedule_due_text, + simpleDateFormat.format(it) + ) + }, + points = getPointsText(assignment.pointsPossible), + type = if (assignment.discussionTopicHeader != null) PlannerItemType.DISCUSSION else PlannerItemType.ASSIGNMENT, + courseName = coursesMap[assignment.courseId]?.name, + courseColor = getCourseColor(coursesMap[assignment.courseId]), + contentDescription = createMissingItemContentDescription(assignment) + ), + open = { + val course = coursesMap[assignment.courseId] + if (course != null) { + if (assignment.discussionTopicHeader != null) { + _events.postValue( + Event( + ScheduleAction.OpenDiscussion( + course, + assignment.discussionTopicHeader!!.id, + assignment.discussionTopicHeader!!.title + ?: "" + ) + ) + ) + } else { + _events.postValue(Event(ScheduleAction.OpenAssignment(course, assignment.id))) + } + } + } + ) + } + return ScheduleMissingItemsGroupItemViewModel(missingItemsPrefs = missingItemsPrefs, items = missingItems) + } + + private fun createMissingItemContentDescription(assignment: Assignment): String { + val typeContentDescription = if (assignment.discussionTopicHeader != null) resources.getString(R.string.a11y_discussion_topic) else resources.getString(R.string.a11y_assignment) + val pointsContentDescription = resources.getQuantityString(R.plurals.a11y_schedule_points, assignment.pointsPossible.toInt(), assignment.pointsPossible) + val dueContentDescription = assignment.dueDate?.let { + resources.getString( + R.string.schedule_due_text, + simpleDateFormat.format(it) + ) + } + val courseContentDescription = resources.getString(R.string.a11y_schedule_course_header_content_description, coursesMap[assignment.courseId]?.name) + + return "$typeContentDescription ${assignment.name} $courseContentDescription $pointsContentDescription $dueContentDescription" + } + + private fun createDayHeader(date: Date, items: List): ScheduleDayGroupItemViewModel { + return ScheduleDayGroupItemViewModel( + getTitleForDate(date), + SimpleDateFormat("MMMM dd", Locale.getDefault()).format(date), + !dateTimeProvider.getCalendar().time.isSameDay(date), + items + ) + } + + private fun getTitleForDate(date: Date): String { + val today = dateTimeProvider.getCalendar().time + if (date.isSameDay(today)) return resources.getString(R.string.today) + if (date.isNextDay(today)) return resources.getString(R.string.tomorrow) + if (date.isPreviousDay(today)) return resources.getString(R.string.yesterday) + return SimpleDateFormat("EEEE", Locale.getDefault()).format(date) + } + + private fun createCourseItems(date: Date): List { + val coursePlannerMap = plannerItems + .filter { + date.isSameDay(it.plannableDate) + } + .groupBy { coursesMap[it.courseId ?: it.plannable.courseId] } + + + val courseViewModels = coursePlannerMap.entries + .sortedBy { it.key?.name } + .map { entry -> + val scheduleViewData = ScheduleCourseViewData( + entry.key?.name ?: resources.getString(R.string.schedule_todo_title), + entry.key != null, + getCourseColor(entry.key), + entry.key?.imageUrl ?: "", + entry.value.map { + createPlannerItemViewModel(it) + } + ) + ScheduleCourseItemViewModel( + scheduleViewData + ) { + entry.key?.let { course -> + _events.postValue(Event(ScheduleAction.OpenCourse(course))) + } + } + } + + return if (courseViewModels.isEmpty()) { + listOf( + ScheduleEmptyItemViewModel( + ScheduleEmptyViewData(resources.getString(R.string.nothing_planned_yet)) + ) + ) + } else { + courseViewModels + } + } + + private fun createChips(plannerItem: PlannerItem): List { + val chips = mutableListOf() + + if (plannerItem.submissionState?.graded == true && plannerItem.submissionState?.excused == false) { + chips.add(PlannerItemTag.Graded) + } + + if (plannerItem.submissionState?.excused == true) { + chips.add(PlannerItemTag.Excused) + } + + if (plannerItem.submissionState?.withFeedback == true) { + chips.add(PlannerItemTag.Feedback) + } + + if (plannerItem.submissionState?.missing == true) { + chips.add(PlannerItemTag.Missing) + } + + if (plannerItem.submissionState?.late == true) { + chips.add(PlannerItemTag.Late) + } + + if (plannerItem.submissionState?.redoRequest == true) { + chips.add(PlannerItemTag.Redo) + } + + if (plannerItem.plannableType == PlannableType.DISCUSSION_TOPIC || plannerItem.plannableType == PlannableType.ANNOUNCEMENT) { + val discussion = discussions[plannerItem.plannable.id] + discussion?.discussionTopicHeader?.unreadCount?.let { unreadCount -> + if (unreadCount > 0) { + chips.add(PlannerItemTag.Replies(unreadCount)) + } + } + } + + return chips.map { + SchedulePlannerItemTagItemViewModel( + SchedulePlannerItemTag( + if (it is PlannerItemTag.Replies) resources.getQuantityString(it.text, it.replyCount, it.replyCount) + else resources.getString(it.text), + resources.getColor(it.color) + ) + ) + } + } + + private fun createPlannerItemViewModel(plannerItem: PlannerItem): SchedulePlannerItemViewModel { + return SchedulePlannerItemViewModel( + SchedulePlannerItemData( + plannerItem.plannable.title, + getTypeForPlannerItem(plannerItem), + getPointsText(plannerItem.plannable.pointsPossible), + getDueText(plannerItem), + isPlannableOpenable(plannerItem), + createContentDescription(plannerItem), + createChips(plannerItem) + ), + plannerItem.plannerOverride?.markedComplete ?: false || plannerItem.submissionState?.submitted ?: false, + { scheduleItemViewModel, markedAsDone -> + updatePlannerOverride( + plannerItem, + scheduleItemViewModel, + markedAsDone + ) + }, + { openPlannable(plannerItem) } + ) + } + + private fun createContentDescription(plannerItem: PlannerItem): String { + val typeContentDescription = createTypeContentDescription(plannerItem.plannableType) + val dateContentDescription = getDueText(plannerItem) + val markedAsDoneContentDescription = + if (plannerItem.plannerOverride?.markedComplete == true || plannerItem.submissionState?.submitted == true) resources.getString(R.string.a11y_marked_as_done) else resources.getString( + R.string.a11y_not_marked_as_done + ) + + return "$typeContentDescription ${plannerItem.plannable.title} $dateContentDescription $markedAsDoneContentDescription" + } + + private fun createTypeContentDescription(plannableType: PlannableType): String { + return when (plannableType) { + PlannableType.ANNOUNCEMENT -> resources.getString(R.string.a11y_announcement) + PlannableType.DISCUSSION_TOPIC -> resources.getString(R.string.a11y_discussion_topic) + PlannableType.CALENDAR_EVENT -> resources.getString(R.string.a11y_calendar_event) + PlannableType.ASSIGNMENT -> resources.getString(R.string.a11y_assignment) + PlannableType.PLANNER_NOTE -> resources.getString(R.string.a11y_planner_note) + PlannableType.QUIZ -> resources.getString(R.string.a11y_quiz) + PlannableType.TODO -> resources.getString(R.string.a11y_todo) + PlannableType.WIKI_PAGE -> resources.getString(R.string.a11y_page) + } + } + + private fun updatePlannerOverride( + plannerItem: PlannerItem, + itemViewModel: SchedulePlannerItemViewModel, + markedAsDone: Boolean + ) { + if (itemViewModel.completed == markedAsDone) return + + viewModelScope.launch { + itemViewModel.apply { + completed = markedAsDone + notifyChange() + } + try { + if (plannerItem.plannerOverride == null) { + val plannerOverride = PlannerOverride( + plannableType = plannerItem.plannableType, + plannableId = plannerItem.plannable.id, + markedComplete = markedAsDone + ) + val createdOverride = + plannerManager.createPlannerOverrideAsync(true, plannerOverride).await().dataOrThrow + plannerItem.plannerOverride = createdOverride + } else { + val updatedOverride = + plannerManager.updatePlannerOverrideAsync(true, markedAsDone, plannerItem.plannerOverride?.id!!) + .await().dataOrThrow + plannerItem.plannerOverride = updatedOverride + } + } catch (e: Exception) { + e.printStackTrace() + itemViewModel.apply { + completed = !markedAsDone + notifyChange() + } + } + } + + } + + private fun openPlannable(plannerItem: PlannerItem) { + when (plannerItem.plannableType) { + PlannableType.ASSIGNMENT -> _events.postValue( + Event( + ScheduleAction.OpenAssignment( + plannerItem.canvasContext, + plannerItem.plannable.id + ) + ) + ) + PlannableType.CALENDAR_EVENT -> _events.postValue( + Event( + ScheduleAction.OpenCalendarEvent( + plannerItem.canvasContext, + plannerItem.plannable.id + ) + ) + ) + PlannableType.DISCUSSION_TOPIC -> _events.postValue( + Event( + ScheduleAction.OpenDiscussion( + plannerItem.canvasContext, + plannerItem.plannable.id, + plannerItem.plannable.title + ) + ) + ) + PlannableType.QUIZ -> { + if (plannerItem.plannable.assignmentId != null) { + // This is a quiz assignment, go to the assignment page + _events.postValue( + Event( + ScheduleAction.OpenAssignment( + plannerItem.canvasContext, + plannerItem.plannable.id + ) + ) + ) + } else { + var htmlUrl = plannerItem.htmlUrl.orEmpty() + if (htmlUrl.startsWith('/')) htmlUrl = apiPrefs.fullDomain + htmlUrl + _events.postValue(Event(ScheduleAction.OpenQuiz(plannerItem.canvasContext, htmlUrl))) + } + } + PlannableType.ANNOUNCEMENT -> _events.postValue( + Event( + ScheduleAction.OpenDiscussion( + plannerItem.canvasContext, + plannerItem.plannable.id, + plannerItem.plannable.title + ) + ) + ) + else -> Unit + } + } + + private fun isPlannableOpenable(plannerItem: PlannerItem): Boolean { + return when (plannerItem.plannableType) { + PlannableType.PLANNER_NOTE -> false + PlannableType.CALENDAR_EVENT -> true + else -> plannerItem.courseId != null + } + } + + private fun getTypeForPlannerItem(plannerItem: PlannerItem): PlannerItemType { + return when (plannerItem.plannableType) { + PlannableType.ASSIGNMENT -> PlannerItemType.ASSIGNMENT + PlannableType.ANNOUNCEMENT -> PlannerItemType.ANNOUNCEMENT + PlannableType.QUIZ -> PlannerItemType.QUIZ + PlannableType.WIKI_PAGE -> PlannerItemType.PAGE + PlannableType.CALENDAR_EVENT -> PlannerItemType.CALENDAR_EVENT + PlannableType.DISCUSSION_TOPIC -> PlannerItemType.DISCUSSION + PlannableType.PLANNER_NOTE -> PlannerItemType.TO_DO + PlannableType.TODO -> PlannerItemType.TO_DO + } + } + + private fun getPointsText(points: Double?): String? { + if (points == null) return null + val numberFormatter = DecimalFormat("##.##") + return resources.getQuantityString(R.plurals.schedule_points, points.toInt(), numberFormatter.format(points)) + } + + private fun getDueText(plannerItem: PlannerItem): String { + return when (plannerItem.plannableType) { + PlannableType.ANNOUNCEMENT -> simpleDateFormat.format(plannerItem.plannableDate) + PlannableType.CALENDAR_EVENT -> getCalendarEventDueText(plannerItem) + PlannableType.PLANNER_NOTE -> resources.getString( + R.string.schedule_todo_due_text, + simpleDateFormat.format(plannerItem.plannable.todoDate.toDate() ?: plannerItem.plannableDate) + ) + else -> resources.getString(R.string.schedule_due_text, simpleDateFormat.format(plannerItem.plannableDate)) + } + } + + private fun getCalendarEventDueText(plannerItem: PlannerItem): String { + val calendarEvent = calendarEvents[plannerItem.plannable.id] + if (calendarEvent?.isAllDay == true) { + return resources.getString(R.string.schedule_all_day_event_text) + } else { + val startText = calendarEvent?.startDate?.let { simpleDateFormat.format(it) } + val endText = calendarEvent?.endDate?.let { simpleDateFormat.format(it) } + if (startText != null && endText != null) { + return resources.getString(R.string.schedule_calendar_event_interval_text, startText, endText) + } + } + + return resources.getString( + R.string.schedule_calendar_event_due_text, + simpleDateFormat.format(plannerItem.plannableDate) + ) + } + + private fun calculateTodayPosition(items: List): Int { + var position = -1 + if (todayHeader != null) { + items.forEach { + position++ + if (it == todayHeader) return position + position += getGroupOpenChildCount(it) + } + } + return position + } + + private fun getGroupOpenChildCount(group: GroupItemViewModel): Int { + var childCount = 0 + if (!group.collapsed) { + childCount += group.items.size + group.items.filterIsInstance().forEach { + childCount += getGroupOpenChildCount(it) + } + } + return childCount + } + + private fun getCourseColor(course: Course?): String { + return when { + !course?.courseColor.isNullOrEmpty() -> course?.courseColor!! + !course?.name.isNullOrEmpty() -> ColorApiHelper.K5_DEFAULT_COLOR + else -> TODO_COLOR + } + } + + fun getTodayRange(): Range? { + todayHeader?.let { + return Range(todayPosition, todayPosition + getGroupOpenChildCount(it)) + } + return null + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/ScheduleCourseItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/ScheduleCourseItemViewModel.kt new file mode 100644 index 0000000000..cd890b669a --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/ScheduleCourseItemViewModel.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.features.elementary.schedule.itemviewmodels + +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.schedule.ScheduleCourseViewData +import com.instructure.pandautils.features.elementary.schedule.ScheduleItemViewModelType +import com.instructure.pandautils.features.elementary.schedule.ScheduleViewData +import com.instructure.pandautils.mvvm.ItemViewModel + +class ScheduleCourseItemViewModel( + val data: ScheduleCourseViewData, + val onHeaderClick: () -> Unit +) : ItemViewModel { + override val layoutId: Int = R.layout.item_schedule_course + + override val viewType: Int = ScheduleItemViewModelType.COURSE.viewType +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/ScheduleDayGroupItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/ScheduleDayGroupItemViewModel.kt new file mode 100644 index 0000000000..27d1094b1d --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/ScheduleDayGroupItemViewModel.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.features.elementary.schedule.itemviewmodels + +import com.instructure.pandautils.R +import com.instructure.pandautils.binding.GroupItemViewModel +import com.instructure.pandautils.features.elementary.schedule.ScheduleItemViewModelType +import com.instructure.pandautils.mvvm.ItemViewModel + +class ScheduleDayGroupItemViewModel( + val dayText: String, + val dateText: String, + val todayVisible: Boolean, + items: List +) : GroupItemViewModel(collapsable = false, items = items) { + + override val layoutId: Int = R.layout.item_schedule_day_header + + override val viewType: Int = ScheduleItemViewModelType.DAY_HEADER.viewType +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/ScheduleEmptyItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/ScheduleEmptyItemViewModel.kt new file mode 100644 index 0000000000..46db2611d3 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/ScheduleEmptyItemViewModel.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.features.elementary.schedule.itemviewmodels + +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.schedule.ScheduleEmptyViewData +import com.instructure.pandautils.features.elementary.schedule.ScheduleItemViewModelType +import com.instructure.pandautils.mvvm.ItemViewModel + +class ScheduleEmptyItemViewModel( + val data: ScheduleEmptyViewData +) : ItemViewModel { + + override val layoutId: Int = R.layout.item_schedule_empty + + override val viewType: Int = ScheduleItemViewModelType.EMPTY.viewType +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/ScheduleMissingItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/ScheduleMissingItemViewModel.kt new file mode 100644 index 0000000000..425d1d461c --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/ScheduleMissingItemViewModel.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.features.elementary.schedule.itemviewmodels + +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.schedule.ScheduleItemViewModelType +import com.instructure.pandautils.features.elementary.schedule.ScheduleMissingItemData +import com.instructure.pandautils.mvvm.ItemViewModel + +class ScheduleMissingItemViewModel( + val data: ScheduleMissingItemData, + val open: () -> Unit +) : ItemViewModel { + override val layoutId: Int = R.layout.item_schedule_missing_item + override val viewType: Int = ScheduleItemViewModelType.MISSING_ITEM.viewType +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/ScheduleMissingItemsGroupItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/ScheduleMissingItemsGroupItemViewModel.kt new file mode 100644 index 0000000000..71b2b2600b --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/ScheduleMissingItemsGroupItemViewModel.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.features.elementary.schedule.itemviewmodels + +import com.instructure.pandautils.R +import com.instructure.pandautils.binding.GroupItemViewModel +import com.instructure.pandautils.features.elementary.schedule.ScheduleItemViewModelType +import com.instructure.pandautils.utils.MissingItemsPrefs + +class ScheduleMissingItemsGroupItemViewModel( + private val missingItemsPrefs: MissingItemsPrefs, + items: List +) : GroupItemViewModel(collapsable = true, collapsed = missingItemsPrefs.itemsCollapsed, items = items) { + override val layoutId: Int = R.layout.item_schedule_missing_header + + override val viewType: Int = ScheduleItemViewModelType.MISSING_HEADER.viewType + + override fun toggleItems() { + super.toggleItems() + missingItemsPrefs.itemsCollapsed = collapsed + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/SchedulePlannerItemTagItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/SchedulePlannerItemTagItemViewModel.kt new file mode 100644 index 0000000000..3a7407938d --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/SchedulePlannerItemTagItemViewModel.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.features.elementary.schedule.itemviewmodels + +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.schedule.PlannerItemTag +import com.instructure.pandautils.features.elementary.schedule.SchedulePlannerItemTag +import com.instructure.pandautils.mvvm.ItemViewModel + +class SchedulePlannerItemTagItemViewModel( + val data: SchedulePlannerItemTag +) : ItemViewModel { + override val layoutId: Int = R.layout.item_schedule_planner_item_tag +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/SchedulePlannerItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/SchedulePlannerItemViewModel.kt new file mode 100644 index 0000000000..338f41f73d --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/itemviewmodels/SchedulePlannerItemViewModel.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.features.elementary.schedule.itemviewmodels + +import androidx.databinding.BaseObservable +import androidx.databinding.Bindable +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.schedule.ScheduleItemViewModelType +import com.instructure.pandautils.features.elementary.schedule.SchedulePlannerItemData +import com.instructure.pandautils.mvvm.ItemViewModel + +class SchedulePlannerItemViewModel( + val data: SchedulePlannerItemData, + @get:Bindable var completed: Boolean, + val markAsDone: (itemViewModel: SchedulePlannerItemViewModel, done: Boolean) -> Unit, + val open: () -> Unit +) : ItemViewModel, BaseObservable() { + override val layoutId: Int = R.layout.item_schedule_planner_item + + override val viewType: Int = ScheduleItemViewModelType.PLANNER_ITEM.viewType +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/pager/SchedulePagerFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/pager/SchedulePagerFragment.kt new file mode 100644 index 0000000000..5a1a5a9442 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/pager/SchedulePagerFragment.kt @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.features.elementary.schedule.pager + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintSet +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.viewpager2.adapter.FragmentStateAdapter +import androidx.viewpager2.widget.ViewPager2 +import com.instructure.pandautils.databinding.FragmentSchedulePagerBinding +import com.instructure.pandautils.features.elementary.schedule.ScheduleFragment +import com.instructure.pandautils.utils.isAccessibilityEnabled +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.android.synthetic.main.fragment_schedule_pager.* + +@AndroidEntryPoint +class SchedulePagerFragment : Fragment() { + + private val viewModel: SchedulePagerViewModel by viewModels() + + private val todayButtonLiveData = MutableLiveData() + + private var todayFragment: ScheduleFragment? = null + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val binding = FragmentSchedulePagerBinding.inflate(inflater, container, false) + binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.data.observe(viewLifecycleOwner, { schedulePagerViewData -> + schedulePagerViewData?.let { + schedulePager.adapter = object : FragmentStateAdapter(this) { + override fun getItemCount(): Int = it.pageStartDates.size + + override fun createFragment(position: Int): Fragment { + val fragment = ScheduleFragment.newInstance(it.pageStartDates[position]) + if (position == THIS_WEEKS_POSITION) { + todayFragment = fragment + } + return fragment + } + + } + } + }) + + viewModel.events.observe(viewLifecycleOwner, { event -> + event.getContentIfNotHandled()?.let { + handleAction(it) + } + }) + + schedulePager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + if (position != THIS_WEEKS_POSITION) { + setTodayButtonVisibility(true) + } + + if (position == 0) { + previousWeekButton.visibility = View.GONE + } else if (position == schedulePager.childCount - 1) { + nextWeekButton.visibility = View.GONE + } else { + previousWeekButton.visibility = View.VISIBLE + nextWeekButton.visibility = View.VISIBLE + } + } + }) + + if (isAccessibilityEnabled(requireContext())) { + movePagerControlToTop() + } + } + + private fun movePagerControlToTop() { + val constraintSet = ConstraintSet() + constraintSet.clone(schedulePage) + constraintSet.clear(controls.id, ConstraintSet.BOTTOM) + constraintSet.clear(schedulePager.id, ConstraintSet.BOTTOM) + constraintSet.clear(schedulePager.id, ConstraintSet.TOP) + constraintSet.connect(controls.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP) + constraintSet.connect(schedulePager.id, ConstraintSet.TOP, controls.id, ConstraintSet.BOTTOM) + constraintSet.connect(schedulePager.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM) + constraintSet.applyTo(schedulePage) + } + + private fun handleAction(action: SchedulePagerAction) { + when (action) { + is SchedulePagerAction.SelectPage -> schedulePager.setCurrentItem(action.position, false) + is SchedulePagerAction.MoveToNext -> schedulePager.setCurrentItem(schedulePager.currentItem + 1, true) + is SchedulePagerAction.MoveToPrevious -> schedulePager.setCurrentItem(schedulePager.currentItem - 1, true) + } + } + + fun getTodayButtonVisibility(): LiveData { + return todayButtonLiveData + } + + fun setTodayButtonVisibility(visible: Boolean) { + todayButtonLiveData.postValue(visible) + } + + fun jumpToToday() { + schedulePager.setCurrentItem(THIS_WEEKS_POSITION, true) + todayFragment?.jumpToToday() + } + + companion object { + fun newInstance() = SchedulePagerFragment() + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/pager/SchedulePagerViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/pager/SchedulePagerViewData.kt new file mode 100644 index 0000000000..a021a6197e --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/pager/SchedulePagerViewData.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.features.elementary.schedule.pager + +data class SchedulePagerViewData(val pageStartDates: List) + +sealed class SchedulePagerAction { + data class SelectPage(val position: Int) : SchedulePagerAction() + object MoveToNext : SchedulePagerAction() + object MoveToPrevious: SchedulePagerAction() +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/pager/SchedulePagerViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/pager/SchedulePagerViewModel.kt new file mode 100644 index 0000000000..17c6c34b30 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/schedule/pager/SchedulePagerViewModel.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.features.elementary.schedule.pager + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.instructure.canvasapi2.utils.toApiString +import com.instructure.pandautils.mvvm.Event +import com.instructure.pandautils.utils.date.DateTimeProvider +import dagger.hilt.android.lifecycle.HiltViewModel +import java.util.* +import javax.inject.Inject + + +const val SCHEDULE_PAGE_COUNT = 53 +const val THIS_WEEKS_POSITION = 27 + +@HiltViewModel +class SchedulePagerViewModel @Inject constructor( + dateTimeProvider: DateTimeProvider +) : ViewModel() { + + val data: LiveData + get() = _data + private val _data = MutableLiveData() + + val events: LiveData> + get() = _events + private val _events = MutableLiveData>() + + init { + val calendar = dateTimeProvider.getCalendar() + calendar.roll(Calendar.WEEK_OF_YEAR, -28) + val startDates = (0..SCHEDULE_PAGE_COUNT).map { + calendar.roll(Calendar.WEEK_OF_YEAR, true) + calendar.time.toApiString() + } + + _data.postValue(SchedulePagerViewData(startDates)) + _events.postValue(Event(SchedulePagerAction.SelectPage(THIS_WEEKS_POSITION))) + } + + fun onPreviousWeekClick() { + _events.postValue(Event(SchedulePagerAction.MoveToPrevious)) + } + + fun onNextWeekClick() { + _events.postValue(Event(SchedulePagerAction.MoveToNext)) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/navigation/WebViewRouter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/navigation/WebViewRouter.kt new file mode 100644 index 0000000000..b0061f4058 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/navigation/WebViewRouter.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.navigation + +interface WebViewRouter { + + fun canRouteInternally(url: String): Boolean + + fun routeInternally(url: String) + + fun openMedia(url: String) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ColorUtils.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ColorUtils.kt index 5d9195085b..b2a9c6f426 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ColorUtils.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ColorUtils.kt @@ -43,4 +43,19 @@ object ColorUtils { canvas.drawBitmap(mutableBitmap, 0f, 0f, paint) return mutableBitmap } + + @JvmStatic + @JvmOverloads + fun parseColor(colorCode: String, defaultColorCode: String = ColorApiHelper.K5_DEFAULT_COLOR): Int { + return try { + val fullColorCode = if (colorCode.length == 4 && colorCode[0].toString() == "#") { + "#${colorCode[1]}${colorCode[1]}${colorCode[2]}${colorCode[2]}${colorCode[3]}${colorCode[3]}" + } else { + colorCode + } + Color.parseColor(fullColorCode) + } catch (e: IllegalArgumentException) { + Color.parseColor(defaultColorCode) + } + } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/DateExtensions.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/DateExtensions.kt index 7ea3698f20..62c6de2f20 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/DateExtensions.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/DateExtensions.kt @@ -20,6 +20,7 @@ import org.threeten.bp.LocalDate import org.threeten.bp.OffsetDateTime import org.threeten.bp.format.DateTimeFormatter import org.threeten.bp.format.DateTimeFormatterBuilder +import java.util.* fun OffsetDateTime.getShortMonthAndDay(): String { // Get year if the year of the due date isn't the current year @@ -30,4 +31,43 @@ fun OffsetDateTime.getShortMonthAndDay(): String { fun OffsetDateTime.getTime(): String { val pattern = DateTimeFormatterBuilder().appendPattern("h:mm a").toFormatter() return format(pattern).toLowerCase() -} \ No newline at end of file +} + +fun Date.isSameDay(date: Date?): Boolean { + if (date == null) return false + val calendar1: Calendar = Calendar.getInstance() + calendar1.time = this + val calendar2: Calendar = Calendar.getInstance() + calendar2.time = date + return calendar1.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR) && calendar1.get(Calendar.MONTH) == calendar2.get(Calendar.MONTH) && calendar1.get(Calendar.DAY_OF_MONTH) == calendar2.get(Calendar.DAY_OF_MONTH) +} + +fun Date.isNextDay(date: Date?): Boolean { + if (date == null) return false + val calendar = Calendar.getInstance() + calendar.time = date + calendar.roll(Calendar.DAY_OF_YEAR, true) + return calendar.time.isSameDay(this) +} + +fun Date.isPreviousDay(date: Date?): Boolean { + if (date == null) return false + val calendar = Calendar.getInstance() + calendar.time = date + calendar.roll(Calendar.DAY_OF_YEAR, false) + return calendar.time.isSameDay(this) +} + +fun Date.getLastSunday(): Date { + val calendar = Calendar.getInstance() + calendar.time = this + calendar.add(Calendar.DAY_OF_WEEK, -(calendar.get(Calendar.DAY_OF_WEEK) - 1)) + return calendar.time +} + +fun Date.getNextSaturday(): Date { + val calendar = Calendar.getInstance() + calendar.time = this + calendar.add(Calendar.DAY_OF_WEEK, Calendar.SATURDAY - calendar.get(Calendar.DAY_OF_WEEK)) + return calendar.time +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/HtmlContentFormatter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/HtmlContentFormatter.kt index e16b6bb17e..87924ce9a9 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/HtmlContentFormatter.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/HtmlContentFormatter.kt @@ -58,15 +58,6 @@ class HtmlContentFormatter( // Handle the LTI case val newIframe = externalToolIframe(srcUrl, iframe, context) newHTML = newHTML.replace(iframe, newIframe) - } else if (srcUrl.contains("media_objects_iframe")) { - // Handle the new RCE iframe case - val dataMediaIdMatcher = Pattern.compile("data-media-id=\"([^\"]+)\"").matcher(iframe) - if (dataMediaIdMatcher.find()) { - val dataMediaId = dataMediaIdMatcher.group(1) - - val newIframe = newRceVideoElement(dataMediaId); - newHTML = newHTML.replace(iframe, newIframe) - } } else if (iframe.contains("id=\"cnvs_content\"")) { // Handle the cnvs_content special case for some schools val authenticatedUrl = authenticateLTIUrl(srcUrl) @@ -115,18 +106,6 @@ class HtmlContentFormatter( return awaitApi { oAuthManager.getAuthenticatedSession(ltiUrl, it) }.sessionUrl } - private fun newRceVideoElement(dataMediaId: String): String { - // We need to make a new src url with the dataMediaId - val newSrcUrl = "/users/self/media_download?entryId=$dataMediaId&media_type=video&redirect=1" - - // We can't just update the src url in the iframe, as iframes always auto load/play the src - return """ - - """.trimIndent() - } - fun createAuthenticatedLtiUrl(html: String, authenticatedSessionUrl: String?): String { if (authenticatedSessionUrl == null) return html // Now we need to swap out part of the old url for this new authenticated url diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/MissingItemsPrefs.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/MissingItemsPrefs.kt new file mode 100644 index 0000000000..5ee5cdaf78 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/MissingItemsPrefs.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.utils + +import com.instructure.canvasapi2.utils.BooleanPref +import com.instructure.canvasapi2.utils.PrefManager + +object MissingItemsPrefs : PrefManager("missing-items-prefs") { + + var itemsCollapsed by BooleanPref() +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ProfileUtils.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ProfileUtils.kt index a9f1a10b8c..25aabfa90d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ProfileUtils.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ProfileUtils.kt @@ -25,7 +25,13 @@ import android.graphics.Typeface import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.view.View +import android.widget.ImageView +import androidx.core.content.ContextCompat import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target import com.instructure.canvasapi2.models.Author import com.instructure.canvasapi2.models.BasicUser import com.instructure.canvasapi2.models.Conversation @@ -92,6 +98,48 @@ object ProfileUtils { } } + fun loadAvatarForUser(imageView: ImageView, name: String?, url: String?) { + val context = imageView.context + if (shouldLoadAltAvatarImage(url)) { + val avatarDrawable = createAvatarDrawable(context, name ?: "") + imageView.setImageDrawable(avatarDrawable) + } else { + Glide.with(imageView) + .load(url) + .placeholder(R.drawable.recipient_avatar_placeholder) + .circleCrop() + .addListener(object : RequestListener { + override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean { + val avatarDrawable = createAvatarDrawable(context, name ?: "") + imageView.setImageDrawable(avatarDrawable) + return false + } + + override fun onResourceReady(resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { + return false + } + + }) + .into(imageView) + } + } + + private fun createAvatarDrawable(context: Context, userName: String): Drawable { + val initials = getUserInitials(userName) + val color = ContextCompat.getColor(context, R.color.gray) + return TextDrawable.builder() + .beginConfig() + .height(context.resources.getDimensionPixelSize(R.dimen.avatar_size)) + .width(context.resources.getDimensionPixelSize(R.dimen.avatar_size)) + .toUpperCase() + .useFont(Typeface.DEFAULT_BOLD) + .textColor(color) + .withBorder(context.DP(0.5f).toInt()) + .withBorderColor(color) + .endConfig() + .buildRound(initials, Color.WHITE) + } + /** * Sets up the provided [avatar] for the given [conversation]. * diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/WebViewExtensions.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/WebViewExtensions.kt index cb0342c4cc..61e06d1eb8 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/WebViewExtensions.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/WebViewExtensions.kt @@ -38,7 +38,6 @@ import java.util.regex.Pattern * This currently handles three iframe cases: * -cnvs_content src authentication * -lti iframe src auth and launch button - * -new rce videos in iframes * * We should now be able to call this function, preceded by a simple check for iframes, for all html webview content */ @@ -65,15 +64,6 @@ fun WebView.loadHtmlWithIframes(context: Context, isTablet: Boolean, html: Strin hasLtiTool = true val newIframe = inBackground { externalToolIframe(srcUrl, iframe, context); } newHTML = newHTML.replace(iframe, newIframe) - } else if(srcUrl.contains("media_objects_iframe")) { - // Handle the new RCE iframe case - val dataMediaIdMatcher = Pattern.compile("data-media-id=\"([^\"]+)\"").matcher(iframe) - if (dataMediaIdMatcher.find()) { - val dataMediaId = dataMediaIdMatcher.group(1) - - val newIframe = newRceVideoElement(dataMediaId); - newHTML = newHTML.replace(iframe, newIframe) - } } else if(iframe.contains("id=\"cnvs_content\"")) { // Handle the cnvs_content special case for some schools val authenticatedUrl = inBackground { authenticateLTIUrl(srcUrl) } @@ -123,18 +113,6 @@ suspend fun externalToolIframe(srcUrl: String, iframe: String, context: Context) return newIframe + htmlButton } -fun newRceVideoElement(dataMediaId: String): String { - // We need to make a new src url with the dataMediaId - val newSrcUrl = "/users/self/media_download?entryId=$dataMediaId&media_type=video&redirect=1" - - // We can't just update the src url in the iframe, as iframes always auto load/play the src - return """ - - """.trimIndent() -} - fun handleLTIPlaceHolders(placeHolderList: ArrayList, html: String): String { var newHtml = html for (holder in placeHolderList) { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/date/DateTimeProvider.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/date/DateTimeProvider.kt new file mode 100644 index 0000000000..d9f1bd57e0 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/date/DateTimeProvider.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.utils.date + +import java.util.* + +interface DateTimeProvider { + + fun getCalendar(): Calendar +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/date/RealDateTimeProvider.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/date/RealDateTimeProvider.kt new file mode 100644 index 0000000000..c07b123eed --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/date/RealDateTimeProvider.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.utils.date + +import java.util.* + +class RealDateTimeProvider : DateTimeProvider { + + override fun getCalendar(): Calendar = Calendar.getInstance() +} \ No newline at end of file diff --git a/libs/pandautils/src/main/res/color/schedule_checkbox_color_selector.xml b/libs/pandautils/src/main/res/color/schedule_checkbox_color_selector.xml new file mode 100644 index 0000000000..78cc3db00f --- /dev/null +++ b/libs/pandautils/src/main/res/color/schedule_checkbox_color_selector.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/drawable/bg_rounded_rectangle.xml b/libs/pandautils/src/main/res/drawable/bg_rounded_rectangle.xml new file mode 100644 index 0000000000..d6ee277edd --- /dev/null +++ b/libs/pandautils/src/main/res/drawable/bg_rounded_rectangle.xml @@ -0,0 +1,27 @@ + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/drawable/bg_rounded_rectangle_filled.xml b/libs/pandautils/src/main/res/drawable/bg_rounded_rectangle_filled.xml new file mode 100644 index 0000000000..1ca36c1cfe --- /dev/null +++ b/libs/pandautils/src/main/res/drawable/bg_rounded_rectangle_filled.xml @@ -0,0 +1,24 @@ + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/drawable/ic_chevron_filled_left.xml b/libs/pandautils/src/main/res/drawable/ic_chevron_filled_left.xml new file mode 100644 index 0000000000..5064ef4adf --- /dev/null +++ b/libs/pandautils/src/main/res/drawable/ic_chevron_filled_left.xml @@ -0,0 +1,10 @@ + + + diff --git a/libs/pandautils/src/main/res/drawable/ic_chevron_filled_right.xml b/libs/pandautils/src/main/res/drawable/ic_chevron_filled_right.xml new file mode 100644 index 0000000000..010a8501ea --- /dev/null +++ b/libs/pandautils/src/main/res/drawable/ic_chevron_filled_right.xml @@ -0,0 +1,10 @@ + + + diff --git a/libs/pandautils/src/main/res/layout-sw720dp/item_grade_row.xml b/libs/pandautils/src/main/res/layout-sw720dp/item_grade_row.xml new file mode 100644 index 0000000000..14ab7a1e97 --- /dev/null +++ b/libs/pandautils/src/main/res/layout-sw720dp/item_grade_row.xml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout-sw720dp/item_schedule_course.xml b/libs/pandautils/src/main/res/layout-sw720dp/item_schedule_course.xml new file mode 100644 index 0000000000..da60b5f0a7 --- /dev/null +++ b/libs/pandautils/src/main/res/layout-sw720dp/item_schedule_course.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout-w720dp/item_schedule_planner_item.xml b/libs/pandautils/src/main/res/layout-w720dp/item_schedule_planner_item.xml new file mode 100644 index 0000000000..ad1427acad --- /dev/null +++ b/libs/pandautils/src/main/res/layout-w720dp/item_schedule_planner_item.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/fragment_grades.xml b/libs/pandautils/src/main/res/layout/fragment_grades.xml index 5be2e3e00a..9d7ff690e7 100644 --- a/libs/pandautils/src/main/res/layout/fragment_grades.xml +++ b/libs/pandautils/src/main/res/layout/fragment_grades.xml @@ -14,10 +14,46 @@ ~ along with this program. If not, see . ~ --> - + - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/fragment_resources.xml b/libs/pandautils/src/main/res/layout/fragment_resources.xml index 47f31fd21e..135417c22a 100644 --- a/libs/pandautils/src/main/res/layout/fragment_resources.xml +++ b/libs/pandautils/src/main/res/layout/fragment_resources.xml @@ -14,10 +14,87 @@ ~ along with this program. If not, see . ~ --> - + - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/fragment_schedule.xml b/libs/pandautils/src/main/res/layout/fragment_schedule.xml index 13567a7a79..ac3f88752c 100644 --- a/libs/pandautils/src/main/res/layout/fragment_schedule.xml +++ b/libs/pandautils/src/main/res/layout/fragment_schedule.xml @@ -14,10 +14,48 @@ ~ along with this program. If not, see . ~ --> - + - \ No newline at end of file + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/fragment_schedule_pager.xml b/libs/pandautils/src/main/res/layout/fragment_schedule_pager.xml new file mode 100644 index 0000000000..c3e132e68e --- /dev/null +++ b/libs/pandautils/src/main/res/layout/fragment_schedule_pager.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/item_announcement.xml b/libs/pandautils/src/main/res/layout/item_announcement.xml index 5f61013170..3a462cc35d 100644 --- a/libs/pandautils/src/main/res/layout/item_announcement.xml +++ b/libs/pandautils/src/main/res/layout/item_announcement.xml @@ -84,6 +84,6 @@ android:layout_width="match_parent" android:layout_height="0.5dp" android:layout_marginBottom="24dp" - android:background="@color/k5AnnouncementSeparatorColor" /> + android:background="@color/k5SeparatorColor" /> \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/item_contact_info.xml b/libs/pandautils/src/main/res/layout/item_contact_info.xml new file mode 100644 index 0000000000..87a15f0fac --- /dev/null +++ b/libs/pandautils/src/main/res/layout/item_contact_info.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/item_course_card.xml b/libs/pandautils/src/main/res/layout/item_course_card.xml index f14b2a689e..7bd1ca5f07 100644 --- a/libs/pandautils/src/main/res/layout/item_course_card.xml +++ b/libs/pandautils/src/main/res/layout/item_course_card.xml @@ -25,7 +25,7 @@ - + @@ -144,7 +144,7 @@ android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:src="@drawable/ic_homeroom_announcement" - android:tint="@{Color.parseColor(itemViewModel.data.courseColor)}" + android:tint="@{ColorUtils.parseColor(itemViewModel.data.courseColor)}" android:importantForAccessibility="no" tools:tint="@color/canvasDefaultAccent" /> diff --git a/libs/pandautils/src/main/res/layout/item_grade_row.xml b/libs/pandautils/src/main/res/layout/item_grade_row.xml new file mode 100644 index 0000000000..3de15707ba --- /dev/null +++ b/libs/pandautils/src/main/res/layout/item_grade_row.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/item_grading_period_selector.xml b/libs/pandautils/src/main/res/layout/item_grading_period_selector.xml new file mode 100644 index 0000000000..974f84f331 --- /dev/null +++ b/libs/pandautils/src/main/res/layout/item_grading_period_selector.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/item_important_links.xml b/libs/pandautils/src/main/res/layout/item_important_links.xml new file mode 100644 index 0000000000..f9b3d58253 --- /dev/null +++ b/libs/pandautils/src/main/res/layout/item_important_links.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/item_lti_application.xml b/libs/pandautils/src/main/res/layout/item_lti_application.xml new file mode 100644 index 0000000000..a987568313 --- /dev/null +++ b/libs/pandautils/src/main/res/layout/item_lti_application.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/item_resources_header.xml b/libs/pandautils/src/main/res/layout/item_resources_header.xml new file mode 100644 index 0000000000..163926e0e3 --- /dev/null +++ b/libs/pandautils/src/main/res/layout/item_resources_header.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/item_schedule_course.xml b/libs/pandautils/src/main/res/layout/item_schedule_course.xml new file mode 100644 index 0000000000..7f168e699b --- /dev/null +++ b/libs/pandautils/src/main/res/layout/item_schedule_course.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/item_schedule_day_header.xml b/libs/pandautils/src/main/res/layout/item_schedule_day_header.xml new file mode 100644 index 0000000000..d31897a28d --- /dev/null +++ b/libs/pandautils/src/main/res/layout/item_schedule_day_header.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/item_schedule_empty.xml b/libs/pandautils/src/main/res/layout/item_schedule_empty.xml new file mode 100644 index 0000000000..28479da423 --- /dev/null +++ b/libs/pandautils/src/main/res/layout/item_schedule_empty.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/item_schedule_missing_header.xml b/libs/pandautils/src/main/res/layout/item_schedule_missing_header.xml new file mode 100644 index 0000000000..7e1f44de3b --- /dev/null +++ b/libs/pandautils/src/main/res/layout/item_schedule_missing_header.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/item_schedule_missing_item.xml b/libs/pandautils/src/main/res/layout/item_schedule_missing_item.xml new file mode 100644 index 0000000000..c1cacda5d8 --- /dev/null +++ b/libs/pandautils/src/main/res/layout/item_schedule_missing_item.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/item_schedule_planner_item.xml b/libs/pandautils/src/main/res/layout/item_schedule_planner_item.xml new file mode 100644 index 0000000000..cb5e00b919 --- /dev/null +++ b/libs/pandautils/src/main/res/layout/item_schedule_planner_item.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/item_schedule_planner_item_tag.xml b/libs/pandautils/src/main/res/layout/item_schedule_planner_item_tag.xml new file mode 100644 index 0000000000..253bb518cb --- /dev/null +++ b/libs/pandautils/src/main/res/layout/item_schedule_planner_item_tag.xml @@ -0,0 +1,43 @@ + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/values-ar/strings.xml b/libs/pandautils/src/main/res/values-ar/strings.xml index 5f50f157a6..d3ce69e2e1 100644 --- a/libs/pandautils/src/main/res/values-ar/strings.xml +++ b/libs/pandautils/src/main/res/values-ar/strings.xml @@ -17,7 +17,6 @@ --> - اختيار الوسائط التقاط فيديو لا توجد كاميرا @@ -42,8 +41,11 @@ إخطارات التحميل إخطارات Canvas لعمليات التحميل الجارية. + + إضافة ملف + الكاميرا المعرض الجهاز @@ -53,7 +55,6 @@ الملف الملف %s المجلد %s - إضافة عنصر المهمة الملفات المرفقة @@ -97,6 +98,7 @@ جاري تحميل الإرسال لـ %s الإرسال فشل لـ %s تم إرسال %s بنجاح + حدث خطأ غير متوقع. حدث خطأ بالخادم. @@ -183,9 +185,9 @@ يبدو أنه لم يتم إنشاء وحدات نمطية حتى الآن. لا توجد صفحات يبدو أنه لم يتم إنشاء أي صفحات حتى الآن. - تم تعطيل وحدات نمطية لهذا المساق. حدث خطأ ما + نعم لا @@ -337,6 +339,7 @@ + لا يمكن أن يكون تاريخ إلغاء التأمين بعد تاريخ الاستحقاق لا يمكن أن يكون تاريخ التأمين قبل تاريخ الاستحقاق لا يمكن أن يكون تاريخ التأمين قبل تاريخ إلغاء التأمين @@ -390,16 +393,17 @@ التحميل إلى تعليق الإرسال إرفاق ملف مع + إخطار Canvas إخطارات Canvas عامة يمكن إجراء تكوين إضافي للإخطارات داخل قسم تفضيلات إخطار Canvas. - %d من الإعلامات الجديدة %d من الإعلامات الجديدة %d من الإعلامات الجديدة %d من الإعلامات الجديدة + نسخ عنوان الارتباط نسخ الرابط تم نسخ الارتباط @@ -429,7 +433,6 @@ جارٍ تحميل الصورة... خطأ في تحميل الصورة إعادة المحاولة - بحث البحث عن المهام البحث عن الإعلانات @@ -437,11 +440,36 @@ البحث عن الصفحات البحث عن الاختبارات الموجزة لا توجد عناصر مطابقة لـ \"%s\" + تم التحديد إضافة إعلامات التحديث إعلامات Canvas للتحديثات داخل التطبيق. التطبيق جاهز للتحديث إعادة تشغيل التطبيق لتثبيت الإصدار الجديد + لم يتم تخطيط أي شيء بعد + تم تقييم الدرجة + ردود + الملاحظات + متأخر + إعادة + معفى + غدًا + أمس + في %s + قائمة المهام: %s + تاريخ الاستحقاق: %s + إظهار %d من العناصر المفقودة + إخفاء %d من العناصر المفقودة + لا يمكن إحضار جدولك + قائمة المهام + + %s نقاط + %s نقطة + %s نقاط + %s نقاط + %s نقاط + %s نقاط + diff --git a/libs/pandautils/src/main/res/values-b+da+instk12/strings.xml b/libs/pandautils/src/main/res/values-b+da+instk12/strings.xml index 64b8fb359a..bf9e7b4132 100644 --- a/libs/pandautils/src/main/res/values-b+da+instk12/strings.xml +++ b/libs/pandautils/src/main/res/values-b+da+instk12/strings.xml @@ -17,7 +17,6 @@ --> - Vælg medie Tag video Intet kamera @@ -42,8 +41,11 @@ Upload meddelelser Canvas-meddelelser til løbende uploads. + + Tilføj fil + Kamera Galleri Enhed @@ -96,6 +98,7 @@ Uploader aflevering til %s Aflevering mislykkedes for %s %s blev indsendt + En uventet fejl opstod. En serverfejl opstod. @@ -182,9 +185,9 @@ Det ser ud til, at der ikke er oprettet nogen forløb endnu. Ingen sier Det ser ud til, at der ikke er oprettet nogen sider endnu. - Forløb er blevet deaktiveret for dette fag Noget gik galt + Ja Nej @@ -325,6 +328,7 @@ + Oplåsningsdatoen kan ikke ligge efter afleveringsdato Låsningsdato kan ikke ligge før afleveringsdatoer Låsningsdato kan ikke ligge før oplåsningsdato @@ -378,13 +382,14 @@ Upload til afleveringskommentar Vedhæft en fil med + Canvas-meddelelse Almindelige Canvas-meddelelser Yderligere konfiguration af meddelelser kan ske i sektionen Canvas-meddelelsesindstillinger. - %d nye meddelelser + Kopier linkets adresse Kopier link Link kopieret @@ -428,5 +433,26 @@ Canvas-meddelelser til opdateringer i appen. App klar til opdatering Genstart appen for at installere den nye version + Intet planlagt endnu + Bedømt + Svar + Feedback + Sen + Annuller fortryd + Undskyldt + I morgen + I går + Kl. %s + Opgaveliste: %s + Forfalder: %s + Vis %d manglende elementer + Skjul %d manglende elementer + Din tidsplan kunne ikke hentes + Opgaveliste + + %s point + %s point + %s point + diff --git a/libs/pandautils/src/main/res/values-b+en+AU+unimelb/strings.xml b/libs/pandautils/src/main/res/values-b+en+AU+unimelb/strings.xml index 8b5e6564aa..236edd72af 100644 --- a/libs/pandautils/src/main/res/values-b+en+AU+unimelb/strings.xml +++ b/libs/pandautils/src/main/res/values-b+en+AU+unimelb/strings.xml @@ -17,7 +17,6 @@ --> - Choose Media Take Video No Camera @@ -42,8 +41,11 @@ Upload Notifications Canvas notifications for ongoing uploads. + + Add File + Camera Gallery Device @@ -53,7 +55,6 @@ File File %s Folder %s - Add Item Assignment Attached Files @@ -97,6 +98,7 @@ Uploading submission for %s Submission failed for %s Successfully submitted %s + An unexpected error occurred. A server error has occurred. @@ -183,9 +185,9 @@ It looks like there aren\'t any modules created yet. No Pages It looks like no pages have been created yet. - Modules have been disabled for this subject. Something Went Wrong + Yes No @@ -326,6 +328,7 @@ + Unlock date cannot be after due date Lock date cannot be before due date Lock date cannot be before unlock date @@ -379,13 +382,14 @@ Upload to Submission Comment Attach a file with + Canvas Notification General Canvas Notifications Further configuration of notifications can be done within the Canvas Notification Preferences section. - %d new notifications + Copy link address Copy Link Link copied @@ -415,7 +419,6 @@ Image Uploading... Image Upload Error Retry - Search Search Assignments Search Announcements @@ -423,11 +426,33 @@ Search Pages Search Quizzes No items match \"%s\" + Selected App update notifications Canvas notifications for in-app updates. App ready to update Restart the app to install the new version + Nothing planned yet + Graded + Replies + Feedback + Late + Redo + Excused + Tomorrow + Yesterday + At %s + To Do: %s + Due: %s + Show %d missing items + Hide %d missing items + Unable to fetch your schedule + To Do + + %s pt + %s pt + %s pts + diff --git a/libs/pandautils/src/main/res/values-b+nb+instk12/strings.xml b/libs/pandautils/src/main/res/values-b+nb+instk12/strings.xml index 37207f734f..22060f8032 100644 --- a/libs/pandautils/src/main/res/values-b+nb+instk12/strings.xml +++ b/libs/pandautils/src/main/res/values-b+nb+instk12/strings.xml @@ -17,7 +17,6 @@ --> - Velg media Ta video Ingen kamera @@ -42,8 +41,11 @@ Varslinger for opplastinger Canvas-varslinger for pågående opplastinger. + + Legg til fil + Kamera Galleri Enhet @@ -96,6 +98,7 @@ Last opp innlevering for %s Kunne ikke innlevere for %s Innlevering fullført %s + Det oppsto en uventet feil. En serverfeil har oppstått. @@ -182,9 +185,9 @@ Det ser ut som det ikke er laget noen moduler ennå. Ingen sider Det ser ut som det ikke er laget noen sider ennå. - Moduler er deaktivert for dette faget. Noe gikk galt + Ja Nei @@ -325,6 +328,7 @@ + Opplåsingsdatoen kan ikke være etter forfallsdato Låsedato kan ikke være før forfallsdato Låsedato kan ikke være før opplåsingsdato @@ -378,13 +382,14 @@ Last opp til sendt inn kommentar Legg ved en fil med + Canvas Notification Generelle Canvas Notifications Ytterligere konfigurering av varsler kan gjøres inne i seksjonen for Canvas Notification Preferences. - %d nye varslinger + Kopier lenke-adresse Kopier lenke Lenke kopiert @@ -428,5 +433,26 @@ Canvas-varslinger for oppdateringer i appen Appen er klar til å oppdateres Start appen på nytt for å installere den nye versjonen + Ingenting planlagt enda + Vurdert + Svar + Tilbakemelding + Sent + Gjør om + Fritatt + I morgen + I går + Den %s + Å gjøre: %s + Frist: %s + Vis %d manglende elementer + Skjul %d manglende elementer + Kunne ikke hente timeplanen din + Å gjøre + + %s poeng + %s poeng + %s poeng + diff --git a/libs/pandautils/src/main/res/values-b+sv+instk12/strings.xml b/libs/pandautils/src/main/res/values-b+sv+instk12/strings.xml index 6d547b084e..fc8e53ad4d 100644 --- a/libs/pandautils/src/main/res/values-b+sv+instk12/strings.xml +++ b/libs/pandautils/src/main/res/values-b+sv+instk12/strings.xml @@ -17,7 +17,6 @@ --> - Välj media Spela in video Ingen kamera @@ -46,6 +45,7 @@ Lägg till fil + Kamera Galleri Enhet @@ -91,7 +91,7 @@ Filikon Laddar upp %d av %d %s har låsts. - Filerna är nu redo att ladda ned. Försök igen. + Filen är nu klar att laddas ner. Försök igen. Filen kan inte laddas ned utan att ge behörighet. Behörighet beviljades. Försök igen.. Välj en fil @@ -121,6 +121,7 @@ En eller flera filer har ett filtillägg som inte är tillåtet Den valda filtypen är inte tillåten. Tillåtna tillägg:  + Kontrollera din dataanslutning och försök igen. Din enhet har inte några program installerade som kan hantera den här filen. @@ -184,9 +185,9 @@ Det verkar inte som om några moduler har skapats än. Inga sidor Det verkar inte som om några sidor har skapats än. - Moduler har inaktiverats för den här kursen. Något gick fel + Ja Nej @@ -327,6 +328,7 @@ + Upplåsningsdatumet kan inte vara efter sista inlämningsdatumet Låsdatumet kan inte vara före inlämningsdatumet Låsdatumet kan inte vara före upplåsningsdatumet @@ -343,7 +345,7 @@ Lägg till ny - Ladda ned bilaga + Ladda ner bilaga Ta bort bilaga @@ -384,10 +386,10 @@ Canvas-avisering Allmänna Canvas-aviseringar Ytterligare konfiguration av aviseringar kan göras i sektionen för inställningar för Canvas-aviseringar. - %d nya aviseringar + Kopiera länkadress Kopiera länk Länken har kopierats @@ -431,5 +433,26 @@ Canvas-aviseringar för uppdateringar i appen. Appen är klar för uppdatering Starta om appen för att installera den nya versionen + Inget planerat ännu + Har bedömts + Svar + Återkoppling + Sen + Gör om + Ursäktad + I morgon + I går + Kl. %s + Att göra: %s + Inlämningsdatum: %s + Visa %d saknade objekt + Dölj %d saknade objekt + Det gick inte att hämta ditt schema + Att göra + + %s poäng + %s poäng + %s poäng + diff --git a/libs/pandautils/src/main/res/values-ca/strings.xml b/libs/pandautils/src/main/res/values-ca/strings.xml index 72f1f7914c..9aa9ab9768 100644 --- a/libs/pandautils/src/main/res/values-ca/strings.xml +++ b/libs/pandautils/src/main/res/values-ca/strings.xml @@ -17,7 +17,6 @@ --> - Tria un element multimèdia Enregistra vídeo Sense càmera @@ -46,6 +45,7 @@ Afegeix un fitxer + Càmera Galeria Dispositiu @@ -121,6 +121,7 @@ Un o més fitxers tenen una extensió de fitxer que no està permesa El tipus de fitxer seleccionat no està permès. Extensions permeses:  + Reviseu la connexió de dades i torneu a provar-ho. El dispositiu no té cap aplicació instal·lada per obrir aquest fitxer. @@ -184,9 +185,9 @@ Sembla que encara no s\'ha creat cap mòdul. No hi ha cap pàgina Sembla que encara no s\'ha creat cap pàgina. - S’han inhabilitat mòduls per a aquest curs. Alguna cosa no ha anat bé + No @@ -327,6 +328,7 @@ + La data de desbloqueig no pot ser posterior a la data de venciment La data de bloqueig no pot ser anterior a la data de venciment La data de bloqueig no pot ser anterior a la data de desbloqueig @@ -384,10 +386,11 @@ Notificació de Canvas Notificacions de Canvas generals A la secció de Preferències de notificacions de Canvas es pot ampliar la configuració de les notificacions. - + %d notificacions noves + Copia l\'adreça de l\'enllaç Copia l\'enllaç Enllaç copiat @@ -431,5 +434,26 @@ Notificació de Canvas d’actualitzacions de l’aplicació. Tot és a punt per fer l’actualització de l’aplicació Reinicieu l’aplicació per instal·lar la versió nova + Encara no hi ha res planificat + Qualificat + Respostes + Suggeriments + Tardà + Refés + Excusat + Demà + Ahir + A les %s + Tasques pendents: %s + Venciment: %s + Mostra %d elements que falten + Amaga %d elements que falten + No es pot obtenir la vostra planificació + Tasques pendents + + %s punt + %s punt + %s punts + diff --git a/libs/pandautils/src/main/res/values-cy/strings.xml b/libs/pandautils/src/main/res/values-cy/strings.xml index 62d6b46145..3e0c8bcb70 100644 --- a/libs/pandautils/src/main/res/values-cy/strings.xml +++ b/libs/pandautils/src/main/res/values-cy/strings.xml @@ -17,7 +17,6 @@ --> - Dewiswch Gyfryngau Gwnewch Fideo Dim Camera @@ -42,8 +41,11 @@ Llwytho Hysbysiadau i fyny Hysbysiadau Canvas ar gyfer ffeiliau sy’n cael eu llwytho i fyny ar hyn o bryd. + + Ychwanegu Ffeil + Camera Oriel Dyfais @@ -53,7 +55,6 @@ Ffeil Ffeil %s Ffolder %s - Ychwanegu Eitem Aseiniad Ffeiliau wedi’u hatodi @@ -97,6 +98,7 @@ Wrthi\'n llwytho cyflwyniad i fyny ar gyfer %s Cyflwyniad wedi methu ar gyfer %s Wedi llwyddo i gyflwyno %s + Gwall annisgwyl. Gwall ar y gweinydd. @@ -183,9 +185,9 @@ Mae’n edrych yn debyg nad oes unrhyw fodiwlau wedi’u creu eto. Dim Tudalennau Mae’n edrych yn debyg nad os tudalennau wedi’u creu eto. - Mae modiwlau wedi’u hanalluogi ar gyfer y cwrs hwn. Aeth rhywbeth o’i le + Iawn Na @@ -326,6 +328,7 @@ + Does dim modd i’r dyddiad datgloi fod ar ôl y dyddiad erbyn Does dim modd i’r dyddiad cloi fod cyn y dyddiad erbyn Does dim modd i’r dyddiad cloi fod cyn y dyddiad datgloi @@ -379,13 +382,14 @@ Llwytho i fyny i Sylw ar y cyflwyniad Atodi ffeil gyda + Hysbysiad Canvas Hysbysiadau Cyffredinol Canvas Gellir ffurfweddu hysbysiadau ymhellach o fewn yr adran Dewisiadau Hysbysiadau Canvas. - %d hysbysiad newydd + Copïo cyfeiriad dolen Copïo Dolen Wedi copïo dolen @@ -415,7 +419,6 @@ Wrthi’n Llwytho’r Ddelwedd i Fyny... Gwall wrth Lwytho’r Ddelwedd i Fyny Rhoi cynnig arall arni - Chwilio Chwilio Aseiniadau Chwilio Cyhoeddiadau @@ -423,11 +426,33 @@ Chwilio Tudalennau Chwilio Cwisiau Dim eitemau’n cyfateb \"%s\" + Wedi dewis Hysbysiadau diweddaru ap Hysbysiadau Canvas ar gyfer diweddariadau mewn ap Ap yn barod i’w ddiweddaru Ail-gychwynnwch yr ap i osod y fersiwn newydd + Does dim byd wedi’i drefnu hyd yma + Wedi graddio + Ateb + Adborth + Yn Hwyr + Ail-wneud + Wedi esgusodi + Yfory + Ddoe + Am %s + Tasgau i’w Gwneud: %s + Erbyn: %s + Dangos %d eitem coll + Cuddio %d eitem coll + Doedd dim modd nôl eich amserlen + Tasgau i’w Gwneud + + %s pwynt + %s pwynt + %s pwynt + diff --git a/libs/pandautils/src/main/res/values-da/strings.xml b/libs/pandautils/src/main/res/values-da/strings.xml index 4207293ce1..49d12ccdd8 100644 --- a/libs/pandautils/src/main/res/values-da/strings.xml +++ b/libs/pandautils/src/main/res/values-da/strings.xml @@ -17,7 +17,6 @@ --> - Vælg medie Tag video Intet kamera @@ -42,8 +41,11 @@ Upload meddelelser Canvas-meddelelser til løbende uploads. + + Tilføj fil + Kamera Galleri Enhed @@ -53,7 +55,6 @@ Fil Fil %s Mappe %s - Tilføj element Opgave Vedhæftede filer @@ -97,6 +98,7 @@ Uploader aflevering til %s Aflevering mislykkedes for %s %s blev indsendt + En uventet fejl opstod. En serverfejl opstod. @@ -183,9 +185,9 @@ Det ser ud til, at der ikke er oprettet nogen moduler endnu. Ingen sier Det ser ud til, at der ikke er oprettet nogen sider endnu. - Moduler er blevet deaktiveret for dette fag. Noget gik galt + Ja Nej @@ -326,6 +328,7 @@ + Oplåsningsdatoen kan ikke ligge efter afleveringsdato Låsningsdato kan ikke ligge før afleveringsdatoer Låsningsdato kan ikke ligge før oplåsningsdato @@ -379,13 +382,14 @@ Upload til afleveringskommentar Vedhæft en fil med + Canvas-meddelelse Almindelige Canvas-meddelelser Yderligere konfiguration af meddelelser kan ske i sektionen Canvas-meddelelsesindstillinger. - %d nye meddelelser + Kopier linkadresse Kopier link Link kopieret @@ -415,7 +419,6 @@ Uploader foto ... Fejl ved upload af foto Prøv igen - Søg Søg opgaver Søg beskeder @@ -423,11 +426,33 @@ Søg sider Søg test Ingen elementer matcher \"%s\" + Valgt Opdateringsmeddelelser for appen Canvas-meddelelser til opdateringer i appen. App klar til opdatering Genstart appen for at installere den nye version + Intet planlagt endnu + Bedømt + Svar + Feedback + Sen + Annuller fortryd + Undskyldt + I morgen + I går + Kl. %s + Opgaveliste: %s + Forfalder: %s + Vis %d manglende elementer + Skjul %d manglende elementer + Din tidsplan kunne ikke hentes + Opgaveliste + + %s point + %s point + %s point + diff --git a/libs/pandautils/src/main/res/values-de/strings.xml b/libs/pandautils/src/main/res/values-de/strings.xml index 863933ae8a..8cbd2474b7 100644 --- a/libs/pandautils/src/main/res/values-de/strings.xml +++ b/libs/pandautils/src/main/res/values-de/strings.xml @@ -17,7 +17,6 @@ --> - Media auswählen Video aufnehmen Keine Kamera @@ -42,8 +41,11 @@ Benachrichtigungen hochladen Canvas-Benachrichtigungen für laufende Uploads + + Datei hinzufügen + Kamera Galerie Gerät @@ -53,7 +55,6 @@ Datei Datei %s Ordner %s - Objekt hinzufügen Aufgabe Angehängte Dateien @@ -97,6 +98,7 @@ Abgabe für %s hochladen Abgabe für %s fehlgeschlagen %s erfolgreich abgegeben + Es ist ein unerwarteter Fehler aufgetreten. Ein Server-Fehler ist aufgetreten. @@ -183,9 +185,9 @@ Es wurden scheinbar noch keine Module erstellt. Keine Seiten Es wurden scheinbar noch keine Seiten erstellt. - Module wurden für diesen Kurs deaktiviert. Etwas ging schief + Ja Kein @@ -326,6 +328,7 @@ + Das Freigabedatum kann nicht nach dem Abgabetermin liegen. Das Sperrdatum darf nicht vor dem Abgabetermin liegen. Das Sperrdatum darf nicht nach dem Freigabedatum liegen. @@ -379,13 +382,14 @@ Hochladen zum Sendekommentar Eine Datei anhängen mit + Canvas-Benachrichtigung Allgemeine Canvas-Benachrichtigungen Die weitere Konfiguration von Benachrichtigungen kann im Abschnitt „Canvas-Benachrichtigungseinstellungen“ vorgenommen werden. - %d neue Benachrichtigungen + Link-Adresse kopieren Link kopieren Link kopiert @@ -415,7 +419,6 @@ Bild wird hochgeladen ... Fehler beim Bildhochladen Erneut versuchen - Suchen Aufgaben suchen Ankündigungen suchen @@ -423,11 +426,33 @@ Seiten suchen Quizze suchen Keine übereinstimmenden Objekte mit \"%s\" + Ausgewählt App-Aktualisierungsbenachrichtigungen Canvas-Benachrichtigungen für App-interne Updates. App zum Update bereit Starten Sie die App erneut, um die neue Version zu installieren. + Noch nichts geplant + Benotet + Antworten + Feedback + Verspätet + Wiederholen + Entschuldigt + Morgen + Gestern + Um %s + Zu erledigen: %s + Fällig: %s + %d fehlende Elemente anzeigen + %d fehlende Elemente ausblenden + Ihr Zeitplan konnte nicht abgerufen werden + Zu erledigen + + %s Pkt. + %s Pkt. + %s Pkte. + diff --git a/libs/pandautils/src/main/res/values-en-rAU/strings.xml b/libs/pandautils/src/main/res/values-en-rAU/strings.xml index dc3b40e4cf..7182d2e6b1 100644 --- a/libs/pandautils/src/main/res/values-en-rAU/strings.xml +++ b/libs/pandautils/src/main/res/values-en-rAU/strings.xml @@ -17,7 +17,6 @@ --> - Choose Media Take Video No Camera @@ -42,8 +41,11 @@ Upload Notifications Canvas notifications for ongoing uploads. + + Add File + Camera Gallery Device @@ -53,7 +55,6 @@ File File %s Folder %s - Add Item Assignment Attached Files @@ -97,6 +98,7 @@ Uploading submission for %s Submission failed for %s Successfully submitted %s + An unexpected error occurred. A server error has occurred. @@ -183,9 +185,9 @@ It looks like there aren\'t any modules created yet. No Pages It looks like no pages have been created yet. - Modules have been disabled for this course. Something Went Wrong + Yes No @@ -326,6 +328,7 @@ + Unlock date cannot be after due date Lock date cannot be before due date Lock date cannot be before unlock date @@ -379,13 +382,14 @@ Upload to Submission Comment Attach a file with + Canvas Notification General Canvas Notifications Further configuration of notifications can be done within the Canvas Notification Preferences section. - %d new notifications + Copy link address Copy Link Link copied @@ -415,7 +419,6 @@ Image Uploading... Image Upload Error Retry - Search Search Assignments Search Announcements @@ -423,11 +426,33 @@ Search Pages Search Quizzes No items match \"%s\" + Selected App update notifications Canvas notifications for in-app updates. App ready to update Restart the app to install the new version + Nothing planned yet + Marked + Replies + Feedback + Late + Redo + Excused + Tomorrow + Yesterday + At %s + To Do: %s + Due: %s + Show %d missing items + Hide %d missing items + Unable to fetch your schedule + To Do + + %s pt + %s pt + %s pts + diff --git a/libs/pandautils/src/main/res/values-en-rCA/strings.xml b/libs/pandautils/src/main/res/values-en-rCA/strings.xml index bf2c43510f..cd00f23d46 100644 --- a/libs/pandautils/src/main/res/values-en-rCA/strings.xml +++ b/libs/pandautils/src/main/res/values-en-rCA/strings.xml @@ -434,5 +434,26 @@ Canvas notifications for in-app updates. App ready to update Restart the app to install the new version + Nothing planned yet + Graded + Replies + Feedback + Late + Redo + Excused + Tomorrow + Yesterday + At %s + To Do: %s + Due: %s + Show %d missing items + Hide %d missing items + Unable to fetch your schedule + To Do + + %s pt + %s pt + %s pts + diff --git a/libs/pandautils/src/main/res/values-en-rCY/strings.xml b/libs/pandautils/src/main/res/values-en-rCY/strings.xml index 8edca27c2d..13c6106e5b 100644 --- a/libs/pandautils/src/main/res/values-en-rCY/strings.xml +++ b/libs/pandautils/src/main/res/values-en-rCY/strings.xml @@ -17,7 +17,6 @@ --> - Choose media Take video No camera @@ -46,6 +45,7 @@ Add file + Camera Gallery Device @@ -121,6 +121,7 @@ One or more files has a file extension that isn\'t allowed The selected file type is not allowed. Allowed extensions:  + Please check your data connection and try again. Your device doesn\'t have any applications installed that can handle this file. @@ -184,9 +185,9 @@ It looks like there aren\'t any units created yet. No pages It looks like no pages have been created yet. - Units have been disabled for this module. Something Went Wrong + Yes No @@ -327,6 +328,7 @@ + Unlock date cannot be after due date Lock date cannot be before due date Lock date cannot be before the unlock date @@ -384,10 +386,10 @@ Canvas notification General Canvas notifications Further configuration of notifications can be done within the Canvas notification preferences section. - %d new notifications + Copy link address Copy link Link copied @@ -431,5 +433,26 @@ Canvas notifications for in-app updates. App ready to update Restart the app to install the new version + Nothing planned yet + Graded + Replies + Feedback + Late + Re-do + Excused + Tomorrow + Yesterday + At %s + To-do: %s + Due: %s + Show %d missing items + Hide %d missing items + Unable to fetch your schedule + To-do + + %s pt + %s pt + %s pts + diff --git a/libs/pandautils/src/main/res/values-en-rGB/strings.xml b/libs/pandautils/src/main/res/values-en-rGB/strings.xml index f726ad5ecd..2609140e33 100644 --- a/libs/pandautils/src/main/res/values-en-rGB/strings.xml +++ b/libs/pandautils/src/main/res/values-en-rGB/strings.xml @@ -17,7 +17,6 @@ --> - Choose media Take video No camera @@ -42,8 +41,11 @@ Upload Notifications Canvas notifications for ongoing uploads. + + Add file + Camera Gallery Device @@ -53,7 +55,6 @@ File File %s Folder %s - Add item Assignment Attached files @@ -97,6 +98,7 @@ Uploading submission for %s Submission failed for %s Successfully submitted %s + An unexpected error occurred. A server error has occurred. @@ -183,9 +185,9 @@ It looks like there aren\'t any modules created yet. No pages It looks like no pages have been created yet. - Modules have been disabled for this course. Something Went Wrong + Yes No @@ -326,6 +328,7 @@ + Unlock date cannot be after due date Lock date cannot be before due date Lock date cannot be before the unlock date @@ -379,13 +382,14 @@ Upload to submission comment Attach a file with + Canvas notification General Canvas notifications Further configuration of notifications can be done within the Canvas notification preferences section. - %d new notifications + Copy link address Copy link Link copied @@ -415,7 +419,6 @@ Image uploading... Image upload error Retry - Search Search assignments Search announcements @@ -423,11 +426,33 @@ Search pages Search quizzes No items match \"%s\" + Selected App update notifications Canvas notifications for in-app updates. App ready to update Restart the app to install the new version + Nothing planned yet + Graded + Replies + Feedback + Late + Re-do + Excused + Tomorrow + Yesterday + At %s + To-do: %s + Due: %s + Show %d missing items + Hide %d missing items + Unable to fetch your schedule + To-do + + %s pt + %s pt + %s pts + diff --git a/libs/pandautils/src/main/res/values-es/strings.xml b/libs/pandautils/src/main/res/values-es/strings.xml index 0ec035d01b..c422fbdbd5 100644 --- a/libs/pandautils/src/main/res/values-es/strings.xml +++ b/libs/pandautils/src/main/res/values-es/strings.xml @@ -17,7 +17,6 @@ --> - Escoger multimedia Tomar video Sin cámara @@ -46,6 +45,7 @@ Agregar archivo + Cámara Galería Dispositivo @@ -121,6 +121,7 @@ Uno o más archivos tienen una extensión de archivo que no se permite No se permite el tipo de archivo seleccionado. Extensiones permitidas:  + Compruebe su conexión de datos y vuelva a intentarlo. Su dispositivo no tiene aplicaciones instaladas que puedan manejar este archivo. @@ -184,9 +185,9 @@ Al parecer no se han creado módulos todavía. No hay páginas Al parecer no se han creado páginas todavía. - Se deshabilitaron los módulos para este curso. Algo salió mal + No @@ -327,6 +328,7 @@ + La fecha de desbloqueo no puede ser posterior a la fecha de entrega La fecha de bloqueo no puede ser anterior a la fecha de entrega La fecha de bloqueo no puede ser anterior a la fecha de desbloqueo @@ -384,10 +386,10 @@ Notificación de Canvas Notificaciones generales de Canvas Se puede realizar una posterior configuración de las notificaciones dentro de la sección de Preferencias de notificaciones de Canvas. - %d nuevas notificaciones + Copiar dirección del enlace Copiar enlace Enlace copiado @@ -431,5 +433,26 @@ Notificaciones de Canvas para actualizaciones en la aplicación. Aplicación lista para ser actualizada Reinicie la aplicación para instalar la nueva versión + Todavía no hay nada planificado + Calificado + Respuestas + Comentarios + Atrasado + Rehacer + Justificado + Mañana + Ayer + A las %s + Por hacer: %s + Fecha límite: %s + Mostrar %d ítems faltantes + Ocultar %d ítems faltantes + No se pudo recuperar su cronograma + Por hacer + + %s pto. + %s pto. + %s ptos. + diff --git a/libs/pandautils/src/main/res/values-fi/strings.xml b/libs/pandautils/src/main/res/values-fi/strings.xml index 9c79b852d8..31f789379b 100644 --- a/libs/pandautils/src/main/res/values-fi/strings.xml +++ b/libs/pandautils/src/main/res/values-fi/strings.xml @@ -17,7 +17,6 @@ --> - Valitse media Ota video Ei kameraa @@ -42,8 +41,11 @@ Latausilmoitukset Canvas-ilmoitukset meneillään olevien latausten tilasta. + + Tiedoston lisäysikkuna + Kamera Galleria Laite @@ -96,6 +98,7 @@ Ladataan lähetystä kohteelle %s Lähetys epäonnistui kohteelle %s Lähetetty onnistuneesti %s + Ilmeni odottamaton virhe. Palvelinvirhe ilmeni. @@ -118,6 +121,7 @@ Yhden tai useamman tiedoston tiedostotunniste ei ole sallittua muotoa Valittu tiedoston tyyppi ei kelpaa. Sallitut tiedostotunnisteet:  + Tarkasta datayhteytesi ja yritä uudelleen. Laitteessasi ei ole asennettuna sovelluksia, jotka voisivat käsitellä tämän tiedoston. @@ -181,9 +185,9 @@ Näyttäisi siltä, että yhtään moduulia ei ole vielä luotu. Ei sivuja Näyttäisi siltä, että sivuja ei ole vielä julkaistu. - Tälle kurssille moduulit on poistettu käytöstä. Jotakin meni pieleen + Kyllä Ei @@ -324,6 +328,7 @@ + Lukituspäivä ei voi olla määräpäivän jälkeen. Lukituspäivä ei voi olla ennen määräpäivää. Lukituspäivä ei voi olla ennen lukituksen poistopäivää. @@ -381,10 +386,10 @@ Canvas-ilmoitus Yleiset Canvas-ilmoitukset Ilmoitusten lisäkonfigurointi voidaan tehdä Canvas-ilmoitusasetusten osassa. - %d uudet ilmoitukset + Kopioi linkin osoite Kopioi linkki Linkki kopioitu @@ -428,5 +433,26 @@ Canvas-ilmoitukset sovelluksen sisäisille päivityksille. Sovellus valmis päivitettäväksi Käynnistä sovellus uudelleen asentaaksesi uusi versio + Ei mitään vielä suunniteltuna + Arvioitu + Vastausta + Palaute + Myöhään + Suorita uudelleen + Vapautettu + Huomenna + Eilen + Kohteessa %s + Tehtävälista: %s + Määräpäivä: %s + Näytä %d puuttuvaa kohdetta + Piilota %d puuttuvaa kohdetta + Aikataulun nouto ei onnistu + Tehtävälista + + %s piste + %s piste + %s pistettä + diff --git a/libs/pandautils/src/main/res/values-fr-rCA/strings.xml b/libs/pandautils/src/main/res/values-fr-rCA/strings.xml index e1d7385c62..eff4283b54 100644 --- a/libs/pandautils/src/main/res/values-fr-rCA/strings.xml +++ b/libs/pandautils/src/main/res/values-fr-rCA/strings.xml @@ -17,7 +17,6 @@ --> - Choisir un média Enregistrer une vidéo Aucune caméra @@ -42,8 +41,11 @@ Notifications de téléversement Notifications de Canvas pour les téléversements en cours. + + Ajouter un fichier + Caméra Galerie Dispositif @@ -53,7 +55,6 @@ Fichier Fichier %s Dossier %s - Ajouter un élément Tâche Fichiers joints @@ -97,6 +98,7 @@ Téléversement de l’envoi pour %s Échec de l’envoi pour %s %s envoyé avec succès! + Une erreur inattendue s’est produite. Une erreur de serveur s\'est produite. @@ -183,9 +185,9 @@ Il semblerait qu\'il n\'y ait aucun module créé pour le moment. Aucune page Il semblerait qu\'aucune page n\'ait encore été créée. - Les modules ont été désactivés pour ce cours. Une erreur est survenue + Oui Non @@ -326,6 +328,7 @@ + La date de déverrouillage ne peut pas être postérieure à la date d’échéance. La date de verrouillage ne peut pas être antérieure à la date d’échéance. La date de verrouillage ne peut pas être antérieure à la date de déverrouillage. @@ -379,13 +382,14 @@ Téléverser au commentaire de soumission Joindre un fichier avec + Notification de Canvas Notifications générales de Canvas D\'autres configurations de notifications peuvent être effectuées dans la section Préférences de notification de Canvas. - %d nouvelles notifications + Copier l’adresse du lien Copier le lien Lien copié @@ -415,7 +419,6 @@ Téléversement d’image(s) en cours... Erreur de téléversement d’image Réessayer - Rechercher Rechercher des tâches Recherche des annonces @@ -423,11 +426,33 @@ Rechercher des pages Rechercher des questionnaires Aucun élément ne correspond à \"%s\" + Sélectionné Notification de mise à jour de l’application Notifications Canvas pour les mises à jour intégrées à l\'application. Application prête à être mise à jour Redémarrez l\'application pour installer la nouvelle version + Rien de planifié en ce moment + Noté + Réponses + Rétroaction + En retard + Rétablir + Exempté + Demain + Hier + À %s + À faire : %s + Échéance : %s + Afficher les %d éléments manquants + Masquer les %d éléments manquants + Impossible d\'obtenir votre horaire + À faire + + %s pt + %s pt + %s pts + diff --git a/libs/pandautils/src/main/res/values-fr/strings.xml b/libs/pandautils/src/main/res/values-fr/strings.xml index ffdc303732..ed6297426d 100644 --- a/libs/pandautils/src/main/res/values-fr/strings.xml +++ b/libs/pandautils/src/main/res/values-fr/strings.xml @@ -17,7 +17,6 @@ --> - Choisir un média Faire une vidéo Aucune caméra @@ -42,8 +41,11 @@ Notifications de téléchargement Notifications Canvas pour les téléchargements en cours. + + Ajouter un fichier + Caméra Gallerie Appareil @@ -53,7 +55,6 @@ Fichier Fichier %s Dossier %s - Ajouter un élément Travaux Fichiers joints @@ -97,6 +98,7 @@ Téléchargement de la soumission pour %s La soumission a échoué pour %s %s soumis avec succès + Une erreur inattendue est survenue. Une erreur de serveur est survenue. @@ -183,9 +185,9 @@ Il semblerait qu\'aucun module n\'ait encore été créé. Aucune page Il semblerait qu\'aucune page n\'ait encore été créée. - Les modules ont été désactivés pour ce cours. Un problème est survenu + Oui Non @@ -326,6 +328,7 @@ + La date de déverrouillage ne peut pas être postérieure à la date d’échéance. La date de verrouillage ne peut pas être antérieure à la date d’échéance. La date de verrouillage ne peut pas être antérieure à la date de déverrouillage. @@ -379,13 +382,14 @@ Télécharger vers le commentaire de soumission Joindre un fichier avec + Notification Canvas Notifications Canvas générales Une configuration plus complète des notifications est disponible dans la section Préférences des notifications Canvas. - %d nouvelles notifications + Copier l\'adresse du lien Copier le lien Lien copié @@ -415,7 +419,6 @@ Téléchargement d\'images... Erreur de téléchargement d\'image Réessayer - Rechercher Chercher des travaux assignés Rechercher des annonces @@ -423,11 +426,33 @@ Rechercher des pages Rechercher des questionnaires Aucun élément ne correspond \"%s\" + Sélectionné Notifications de mise à jour de l’application Notifications Canvas de mises à jour in-app. Application prête à la mise à jour Redémarrez l\'application pour installer la nouvelle version. + Aucune planification pour le moment. + Noté + Réponses + Commentaire + En retard + Refaire + Excusé + Demain + Hier + À %s + À faire : %s + À rendre le : %s + Afficher %d éléments manquants + Masquer %d éléments manquants + Impossible de récupérer votre emploi du temps + À faire + + %s pt + %s pt + %s pts + diff --git a/libs/pandautils/src/main/res/values-ht/strings.xml b/libs/pandautils/src/main/res/values-ht/strings.xml index 2bc2c585fc..b492fef01e 100644 --- a/libs/pandautils/src/main/res/values-ht/strings.xml +++ b/libs/pandautils/src/main/res/values-ht/strings.xml @@ -17,7 +17,6 @@ --> - Chwazi Medya Anrejistre Videyo Pa gen Kamera @@ -42,8 +41,11 @@ Notifikasyon Transfè Notifikasyon Canvas pou transfè k ap fèt. + + Ajoute Fichye + Kamera Galeri Aparèy @@ -53,7 +55,6 @@ Fichye Fichye %s Dosye %s - Ajoute Eleman Sesyon Fichye Ajoute @@ -97,6 +98,7 @@ Transfè soumisyon pou %s Soumisyon echwe pou %s Soumisyon reyisi %s + Yon erè fèt sanzatann. Gen yon erè sèvè ki fèt. @@ -183,9 +185,9 @@ Ta sanble poko gen modil ki kreye. Okenn Paj Ta sanble poko gen paj ki kreye. - Yo dezaktive modil yo pou kou sa a. Gen yon Bagay ki Pase Mal + Wi Non @@ -326,6 +328,7 @@ + Dat deblokaj la pa kapab aprè delè a Dat blokaj la pa dwe anvan delè a Dat blokaj la pa dwe anvan dat deblokaj la @@ -379,13 +382,14 @@ Soumèt nan Kòmantè Soumisyon Ajoute yon fichye avèk + Avètisman Canvas Avètisman Jeneral Canvas Plis konfigirasyon avètisman posib nan seksyon Preferans Avètisman Canvas la. - %d Nouvo notifikasyon + Kopye lyen Kopye Lyen Lyen kopye @@ -415,7 +419,6 @@ Transfè Imaj... Erè Transfè Imaj Re eseye - Chèche Chèche Sesyon Chèche Anons @@ -423,11 +426,33 @@ Chèche Paj Chèche Quiz Pa gen eleman ki koresponn \"%s\" + Chwazi Notifikasyon aktyalizasyon app Notifikasyon Canvas pou achte nan app la. App pare pou aktyalize Relanse app la pou ka enstale nouvo vèsyon an + Anyen poko planifye + Klase + Repons + Kòmantè + An reta + Refè + Egzante + Demen + + Nan %s + Tach: %s + Delè: %s + Afiche %d eleman ki manke + Kache %d eleman ki manke + Enposib pou rekipere agenda w la + Pou Fè + + %s pwen + %s pwen + %s pwen + diff --git a/libs/pandautils/src/main/res/values-is/strings.xml b/libs/pandautils/src/main/res/values-is/strings.xml index cd5fb2dc0e..d17f9b8b03 100644 --- a/libs/pandautils/src/main/res/values-is/strings.xml +++ b/libs/pandautils/src/main/res/values-is/strings.xml @@ -17,7 +17,6 @@ --> - Velja miðil Taka myndband Engin myndavél @@ -46,6 +45,7 @@ Bæta við skrá + Myndavél Gallerí Tæki @@ -121,6 +121,7 @@ Ein eða fleiri af skránum er með nafnauka skrár sem er ekki leyfð Valin skráargerð er ekki leyfileg. Leyfðir nafnaukar:  + Athugaðu gagnatengingu þína og reyndu aftur. Tækið er ekki með uppsett app sem getur opnað þessa skrá. @@ -184,9 +185,9 @@ Það lítur ekki út fyrir að einingar hafi verið búnar til. Engar síður Engin síður virðast hafa verið stofnaðar enn. - Slökkt hefur verið á námsefni fyrir þetta námskeið. Eitthvað fór úrskeiðis + Nei @@ -327,6 +328,7 @@ + Aflæsingardagur getur ekki verið eftir skiladag Læsingardagur getur ekki verið fyrir skiladag Læsingardagur getur ekki verið fyrir opnunardag @@ -384,10 +386,10 @@ Canvas-tilkynningar Almennar Canvas-tilkynningar Frekari fínstillingar á tilkynningum má gera innan kjörstillingahluta Canvas-tilkynninga. - %d nýjar tilkynningar + Afrita slóð tengils Afrita tengil Tengill afritaður @@ -431,5 +433,26 @@ Canvas tilkynningar vegna uppfærslu í smáforriti. Smáforrit tilbúið til uppfærslu Endurræstu smáforritið til að setja upp nýja útgáfu + Ekkert skipulagt ennþá + Metið + Svör + Endurgjöf + Seint + Endurtaka + Undanþegið + Á morgun + Í gær + Á %s + Verkefni: %s + Skil: %s + Sýna %d hluti sem vantar + Fela %d hluti sem vantar + Get ekki náð í stundaskrá þína + Verkefnalisti + + %s punktur + %s punktur + %s punktar + diff --git a/libs/pandautils/src/main/res/values-it/strings.xml b/libs/pandautils/src/main/res/values-it/strings.xml index 719f0321c9..76a63af84c 100644 --- a/libs/pandautils/src/main/res/values-it/strings.xml +++ b/libs/pandautils/src/main/res/values-it/strings.xml @@ -17,7 +17,6 @@ --> - Scegli supporto multimediale Realizza video Nessuna videocamera @@ -46,6 +45,7 @@ Aggiungi file + Videocamera Galleria Dispositivo @@ -121,6 +121,7 @@ Uno o più file hanno un’estensione file non consentita Il tipo di file selezionato non è consentito. Estensioni consentite:  + Verificare la connessione dati e riprovare. Il tuo dispositivo non ha alcuna applicazione installata in grado di gestire questo file. @@ -184,9 +185,9 @@ Sembra che non sia stato ancora creato alcun modulo. Nessuna pagina Sembra che non siano ancora state create delle pagine. - I moduli sono stati disabilitati per questo corso. Si è verificato un problema + No @@ -327,6 +328,7 @@ + La data di sblocco non può essere successiva alla data di scadenza La data di blocco non può essere precedente alla data di scadenza La data di blocco non può essere precedente alla data di sblocco @@ -384,10 +386,10 @@ Notifica Canvas Notifiche Canvas generali Un\'ulteriore configurazione delle notifiche può eseguita all\'interno della sezione Preferenze notifiche Canvas. - %d nuove notifiche + Copia indirizzo link Copia link Link copiato @@ -431,5 +433,26 @@ Notifiche Canvas per aggiornamenti in-app. App pronta per l’aggiornamento Riavvia l’app per installare la nuova versione + Ancora nulla programmato + Valutato + Risposte + Feedback + In ritardo + Ripeti + Giustificato + Domani + Ieri + Alle %s + Elenco attività: %s + Scadenza: %s + Mostra %d elementi mancanti + Nascondi %d elementi mancanti + Impossibile recuperare programma + Elenco attività + + %s pt + %s pt + %s pt. + diff --git a/libs/pandautils/src/main/res/values-ja/strings.xml b/libs/pandautils/src/main/res/values-ja/strings.xml index 6b1f954ada..80e3fd74e8 100644 --- a/libs/pandautils/src/main/res/values-ja/strings.xml +++ b/libs/pandautils/src/main/res/values-ja/strings.xml @@ -17,7 +17,6 @@ --> - メディアを選択 動画を撮影する カメラが使用出来ません @@ -42,8 +41,11 @@ アップロード通知 アップロード中の Canvas 通知。 + + ファイルを追加 + カメラ ギャラリー デバイス @@ -53,7 +55,6 @@ ファイル ファイル%s フォルダ%s - アイテム 課題 添付されたファイル @@ -97,6 +98,7 @@ %s の提出をアップロードしています %s の提出に失敗しました %s の提出に成功しました + 予期しないエラーが発生しました。 サーバーエラーが発生しました。 @@ -183,9 +185,9 @@ まだ作成されているモジュールはないようです。 ページなし まだページが作成されていないようです。 - このコースではモジュールが無効になっています。 なにかが失敗しました + はい いいえ @@ -323,6 +325,7 @@ + ロック解除日を締切日より後にすることはできません ロック日を締切日より前にすることはできません ロック日をロック解除日より前にすることはできません @@ -376,13 +379,14 @@ コメント送信をアップロード ファイルを添付する + Canvas 通知 一般的な Canvas 通知 通知の詳細設定は、Canvas キャンバス通知設定のセクションで行うことができます。 - %dの新規通知 + リンクアドレスをコピー リンクをコピー リンクがコピーされました @@ -412,7 +416,6 @@ 画像をアップロード中... 画像アップロードエラー 再試行 - 検索 課題の検索 お知らせ検索 @@ -420,11 +423,32 @@ ページ検索 クイズ検索 \"%s\"にマッチするアイテムなし + 選択されました アプリの更新通知 アプリ内更新の Canvas 通知。 更新の準備ができているアプリ アプリを再開して、新しいバージョンをインストールする + 計画はまだありません + 採点済み + 返信 + フィードバック + 提出遅れ + やり直し + 免除 + 明日 + 昨日 + %sで + タスク:%s + 期限:%s + %d欠如アイテムを表示する + %d欠如アイテムを非表示にする + スケジュールを取得できません + タスク + + %s 点 + %s 点 + diff --git a/libs/pandautils/src/main/res/values-mi/strings.xml b/libs/pandautils/src/main/res/values-mi/strings.xml index 2d54c72a96..bbe2383935 100644 --- a/libs/pandautils/src/main/res/values-mi/strings.xml +++ b/libs/pandautils/src/main/res/values-mi/strings.xml @@ -17,7 +17,6 @@ --> - Kōwhiri pāpāho Tangohia ataata Kaore he Kāmera @@ -42,8 +41,11 @@ Kaweake ngā whakamōhiotanga Canvas whakamōhitanga mo ngā kaweake e haere tonu ana + + Tāpiri Kōnae + kāmera Taiwhanga pūrere @@ -96,6 +98,7 @@ Kaweake ana nghā whakamōhitanga mo %s I hapa te tapaetanga mo %s I pai te tuku %s + He hapa ohorere i puta. He hapa tūmau i puta @@ -182,9 +185,9 @@ Ko te āhua nei kaore anō ngā kōwae i hangaia. Kaore ngā Whārangi Ko te āhua nei kaore anō ngā whārangi i hangaia. - Kaore ngā kōwae i whakahohetia mō tēnei akoranga. I raruraru tētahi mea + Ae kahore @@ -325,6 +328,7 @@ + E kore e taea e iriti te rā i muri i te rā tika E kore e taea e Maukati te rā kia mua i te rā e tika ana E kore e taea e Maukati te rā i mua i te rā e wetekina ana @@ -378,13 +382,14 @@ Tukuake kī Tapaetanga Kōrero Tāpiri he kōnae ki + Canvas Whakamohiotanga Ngā Canvas Whānui Whakamōhiotanga Ka taea te whakahohe i ētahi atu whakamōhiotanga i roto i te wāhanga Canvas Whakamōhio Hiahiatanga. - %d whakamōhiotanga whakahou + Tārua wāhitau hono Tārua hononga Kua tāruatia te hono @@ -428,5 +433,26 @@ Canvas – ngā whakamōhiotanga mō roto taupānga whakahoutanga Taupānga kua reri mo te whakahou Tīmata anō te taupānga ki te whakauru te wāhanga hou + Heoi kaore he mahere + Kōekehia + Ngā whakautu + Urupare + Tūreiti + Mahi anō + Whakawātea + Apopo + Inanahi + Ī %s + Hei Mahi: %s + E tika ana: %s + Whakāturia %d ngā tūemi e ngaro ana + Huna %d ngā tuemi e ngaro ana + Kaore e taea te tiki i tō pūwhakarite + Hei mahi + + %s koinga + %s koinga + %s ngā koinga + diff --git a/libs/pandautils/src/main/res/values-nb/strings.xml b/libs/pandautils/src/main/res/values-nb/strings.xml index 678e2e4085..231ea8ec70 100644 --- a/libs/pandautils/src/main/res/values-nb/strings.xml +++ b/libs/pandautils/src/main/res/values-nb/strings.xml @@ -17,7 +17,6 @@ --> - Velg media Ta video Ingen kamera @@ -42,8 +41,11 @@ Varslinger for opplastinger Canvas-varslinger for pågående opplastinger. + + Legg til fil + Kamera Galleri Enhet @@ -53,7 +55,6 @@ Fil Fil %s Mapper %s - Legg til punkt Oppgave Vedlagte filer @@ -97,6 +98,7 @@ Last opp innlevering for %s Kunne ikke innlevere for %s Innlevering fullført %s + Det oppsto en uventet feil. En serverfeil har oppstått. @@ -183,9 +185,9 @@ Det ser ut som det ikke er laget noen emner ennå. Ingen sider Det ser ut som det ikke er laget noen sider ennå. - Moduler er deaktivert for dette emnet. Noe gikk galt + Ja Nei @@ -326,6 +328,7 @@ + Opplåsingsdatoen kan ikke være etter forfallsdato Låsedato kan ikke være før forfallsdato Låsedato kan ikke være før opplåsingsdato @@ -379,13 +382,14 @@ Last opp til sendt inn kommentar Legg ved en fil med + Canvas Notification Generelle Canvas Notifications Ytterligere konfigurering av varsler kan gjøres inne i seksjonen for Canvas Notification Preferences. - %d nye varslinger + Kopier lenkeadresse Kopiert lenke Lenke kopiert @@ -415,7 +419,6 @@ Bilde lastes opp... Avvik i opplasting av bilde Forsøk igjen - Søk Oppgavesøk Søk kunngjøringer @@ -423,11 +426,33 @@ Søk sider Søk tester Ingen matchende punkter \"%s\" + Valgt Varslinger om app-oppdatering Canvas-varslinger for oppdateringer i appen Appen er klar til å oppdateres Start appen på nytt for å installere den nye versjonen + Ingenting planlagt enda + Vurdert + Svar + Tilbakemelding + Sent + Gjør om + Fritatt + I morgen + I går + Den %s + Å gjøre: %s + Frist: %s + Vis %d manglende elementer + Skjul %d manglende elementer + Kunne ikke hente timeplanen din + Å gjøre + + %s poeng + %s poeng + %s poeng + diff --git a/libs/pandautils/src/main/res/values-nl/strings.xml b/libs/pandautils/src/main/res/values-nl/strings.xml index e319a29504..00555462eb 100644 --- a/libs/pandautils/src/main/res/values-nl/strings.xml +++ b/libs/pandautils/src/main/res/values-nl/strings.xml @@ -17,7 +17,6 @@ --> - Media kiezen Video maken Geen camera @@ -42,8 +41,11 @@ Meldingen voor uploaden Canvas-meldingen voor actieve uploads. + + Een bestand toevoegen + Camera Galerij Apparaat @@ -53,7 +55,6 @@ Bestand Bestand %s Map %s - Item toevoegen Opdracht Bijgevoegde bestanden @@ -97,6 +98,7 @@ Bezig met uploaden van inlevering voor %s Inlevering mislukt voor %s Inlevering van %s is gelukt + Er is een onverwachte fout opgetreden. Er is een serverfout opgetreden. @@ -183,9 +185,9 @@ Het lijkt erop dat er nog geen modules zijn gemaakt. Geen pagina\'s Het lijkt erop dat er nog geen pagina\'s zijn gemaakt. - Er zijn voor deze cursus modules uitgeschakeld. Er is iets misgegaan + Ja Nee @@ -326,6 +328,7 @@ + Vergrendeldatum kan niet na inleverdatum vallen Vergrendeldatum kan niet voor vervaldatum vallen Vergrendeldatum kan niet voor ontgrendeldatum vallen @@ -379,13 +382,14 @@ Uploaden naar Opmerking bij inlevering Bestand bijvoegen met + Canvas-melding Algemene Canvas-meldingen Verdere configuratie van meldingen kan worden uitgevoerd in de sectie Voorkeuren Canvas-meldingen. - %d nieuwe meldingen + Linkadres kopiëren Link kopieren Link gekopieerd @@ -415,7 +419,6 @@ Afbeelding wordt geüpload... Fout tijdens afbeelding uploaden Opnieuw proberen - Zoeken Zoek opdrachten Aankondigingen zoeken @@ -423,11 +426,33 @@ Pagina\'s zoeken Toetsen zoeken Er komen geen items overeen met \"%s\" + Geselecteerd Meldingen voor app-update Canvas-meldingen voor in-app updates. App klaar voor update Start de app opnieuw om de nieuwe versie te installeren + Nog niets gepland + Beoordeeld + Antwoorden + Feedback + Te laat + Opnieuw + Vrijgesteld + Morgen + Gisteren + Om %s + Takenlijst: %s + Inleverdatum: %s + %d ontbrekende items tonen + %d ontbrekende items verbergen + Kan je planning niet ophalen + Takenlijst + + %s pt + %s pt + %s punten + diff --git a/libs/pandautils/src/main/res/values-pl/strings.xml b/libs/pandautils/src/main/res/values-pl/strings.xml index 09e06ff76f..9ee54bb84b 100644 --- a/libs/pandautils/src/main/res/values-pl/strings.xml +++ b/libs/pandautils/src/main/res/values-pl/strings.xml @@ -17,7 +17,6 @@ --> - Wybierz media Nagraj wideo Brak kamery @@ -42,8 +41,11 @@ Powiadomienia dotyczące przesyłek Powiadomienia Canvas dotyczące przesyłanych obecnie plików. + + Dodaj plik + Kamera Galeria Urządzenie @@ -53,7 +55,6 @@ Plik Plik %s Folder %s - Dodaj element Zadanie Załączone pliki @@ -97,6 +98,7 @@ Przesyłanie plików dla %s Nie udało się przesłać plików dla %s Przesłano pomyślnie %s + Wystąpił nieoczekiwany błąd. Wystąpił błąd serwera. @@ -183,9 +185,9 @@ Wygląda na to, że nie utworzono jeszcze modułów. Brak stron Wygląda na to, że nie utworzono jeszcze stron. - Wyłączono moduły dla tego kursu. Coś poszło nie tak + Tak Nie @@ -332,6 +334,7 @@ + Data odblokowania nie może przypadać po terminie dostarczenia Data zablokowania nie może przypadać przed terminem dostarczenia Data zablokowania nie może przypadać przed datą odblokowania @@ -385,15 +388,16 @@ Prześlij do komentarzy Dołącz plik z + Powiadomienie Canvas Ogólne powiadomienia Canvas Dalszą konfigurację powiadomień można wykonać w sekcji Preferencje powiadomień Canvas. - Nowe powiadomienia: %d Nowe powiadomienia: %d Nowe powiadomienia: %d + Kopiuj adres łącza Kopiuj łącze Skopiowano łącze @@ -423,7 +427,6 @@ Przesyłanie obrazu... Błąd przesyłania obrazu Ponów próbę - Wyszukaj Wyszukaj zadania Wyszukaj ogłoszenia @@ -431,11 +434,35 @@ Wyszukaj strony Wyszukaj testy Nie znaleziono pasujących do \"%s\" + Wybrano Powiadomienia o aktualizacji w aplikacji Powiadomienia Canvas dotyczące aktualizacji dostępnych w aplikacji. Aplikacja gotowa do zaktualizowania Uruchom ponownie aplikację, aby zainstalować nową wersję. + Nic jeszcze nie zaplanowano + Oceniono + Odpowiedzi + Informacje zwrotne + Późno + Cofnij + Usprawiedliwiony + Jutro + Wczoraj + O %s + Lista zadań: %s + Termin: %s + Pokaż brakujące elementy: %d + Ukryj brakujące elementy: %d + Nie można pobrać harmonogramu + Lista zadań + + %s pkt + %s pkt + %s pkt + %s pkt + %s pkt + diff --git a/libs/pandautils/src/main/res/values-pt-rBR/strings.xml b/libs/pandautils/src/main/res/values-pt-rBR/strings.xml index 64b5cced4d..529621e626 100644 --- a/libs/pandautils/src/main/res/values-pt-rBR/strings.xml +++ b/libs/pandautils/src/main/res/values-pt-rBR/strings.xml @@ -17,7 +17,6 @@ --> - Escolher Mídia Fazer Vídeo Nenhuma Câmera @@ -46,6 +45,7 @@ Adicionar arquivo + Câmera Galeria Dispositivo @@ -121,6 +121,7 @@ Um ou mais arquivos tem uma extensão de arquivo que não é permitida O tipo de arquivo selecionado não é permitido. Extensões permitidas:  + Por favor, verifique sua conexão de dados e tente novamente. O seu dispositivo não tem nenhum aplicativo instalado que pode lidar com esse arquivo. @@ -184,9 +185,9 @@ Parece que ainda não há quaisquer módulos criados. Sem páginas Parece que nenhuma página foi criada ainda. - Os módulos foram desativados para este curso. Algo deu errado + Sim Não @@ -327,6 +328,7 @@ + A data de desbloqueio não pode ser posterior ao prazo de entrega A data de bloqueio não pode ser anterior ao prazo de entrega A data de bloqueio não pode ser anterior à data de desbloqueio @@ -384,10 +386,10 @@ Notificação do Canvas Notificações gerais do Canvas Uma maior configuração de notificações pode ser feita dentro da turma de Preferências de Notificação do Canvas. - %d novas notificações + Copiar endereço do link Copiar Link Link copiado @@ -431,5 +433,26 @@ Notificações do Canvas para atualizações no aplicativo. Aplicativo pronto para atualizar Reinicie o aplicativo para instalar a nova versão + Nada planejado ainda + Avaliado + Respostas + Feedback + Atrasado + Refazer + Dispensado + Amanhã + Ontem + Em %s + Para fazer: %s + Vencimento: %s + Mostrar %d itens ausentes + Ocultar %d itens ausentes + Incapaz de buscar sua programação + Lista de Tarefas + + %s pt + %s pt + %s pts + diff --git a/libs/pandautils/src/main/res/values-pt-rPT/strings.xml b/libs/pandautils/src/main/res/values-pt-rPT/strings.xml index d89139f1b9..678a96c1fb 100644 --- a/libs/pandautils/src/main/res/values-pt-rPT/strings.xml +++ b/libs/pandautils/src/main/res/values-pt-rPT/strings.xml @@ -17,7 +17,6 @@ --> - Escolher Multimédia Fazer Vídeo Sem Câmara @@ -42,8 +41,11 @@ Notificações de transferência Notificações de exrã para transferências em andamento. + + Adicionar ficheiro + Câmara Galeria Dispositivo @@ -53,7 +55,6 @@ Ficheiro Ficheiro %s Pasta %s - Adicionar Item Tarefa Ficheiros Anexados @@ -97,6 +98,7 @@ Enviando envio para %s A submissão falhou para %s Submetido com sucesso %s + Ocorreu um erro inesperado. Ocorreu um erro no servidor. @@ -183,9 +185,9 @@ Parece que não há nenhum módulo criado ainda. Sem páginas Parece que nenhuma página foi criada ainda. - Módulos foram definidos para esta disciplina. Algo deu errado + Sim Não @@ -326,6 +328,7 @@ + A data de desbloqueio não pode ser posterior à data de entrega A data de bloqueio não pode ser anterior à data de entrega A data de bloqueio não pode ser anterior à data de desbloqueio @@ -379,13 +382,14 @@ Carregar para comentário de envio Anexar um ficheiro com + Notificação Canvas Notificações gerais do Canvas A configuração adicional das notificações pode ser feita dentro da seção de Preferências de Notificação de Canvas. - %d novas notificações + Copiar endereço da ligação Copiar ligação Ligação copiada @@ -415,7 +419,6 @@ Carregamento de imagem... Erro ao carregar imagem Tentar novamente - Pesquisar Pesquisar Tarefas Pesquisar Anúncios @@ -423,11 +426,33 @@ Pesquisar Páginas Pesquisar testes Nenhum item corresponde \"%s\" + Selecionado Notificações de atualização de aplicações Notificações de lona para atualizações no Canvas. Aplicação pronta a actualizar Reiniciar a aplicação para instalar a nova versão + Nada planeado ainda + Classificado + Respostas + Resposta + Atrasado + Refazer + Desculpado + Amanhã + Ontem + Em %s + Tarefa: %s + Vencimento: %s + Mostrar %d itens em falta + Ocultar %d itens em falta + Incapaz de ir buscar a sua agenda + A Fazer + + %s pt + %s pt + %s pts + diff --git a/libs/pandautils/src/main/res/values-ru/strings.xml b/libs/pandautils/src/main/res/values-ru/strings.xml index d5ffeb0156..fd6c0f5a45 100644 --- a/libs/pandautils/src/main/res/values-ru/strings.xml +++ b/libs/pandautils/src/main/res/values-ru/strings.xml @@ -17,7 +17,6 @@ --> - Выбрать медиа Снять видео Нет камеры @@ -42,8 +41,11 @@ Загрузить уведомления Уведомления Canvas для текущих загрузок. + + Добавить файл + Камера Галерея Устройство @@ -53,7 +55,6 @@ Файл Файл %s Папка %s - Добавить элемент Задание Прикрепленные файлы @@ -97,6 +98,7 @@ Загрузка отправки для %s Сбой отправки для %s Успешно отправлено %s + Произошла непредвиденная ошибка. Произошла ошибка сервера. @@ -183,9 +185,9 @@ Похоже, что модули еще не создавались. Страницы отсутствуют Похоже, что страницы еще не создавались. - Модули были отключены для этого курса. Что-то пошло не так + Да Нет @@ -332,6 +334,7 @@ + Дата разблокирования не может быть позже даты выполнения Дата блокировки не может быть раньше даты выполнения Дата блокирования не может быть раньше даты разблокирования @@ -385,15 +388,16 @@ Загрузить в комментарий к отправке Прикрепить файл с + Уведомление Canvas Общие уведомления Canvas Дополнительная настройка уведомлений осуществляется в разделе настроек уведомлений Canvas. - %d новых уведомлений %d новых уведомлений %d новых уведомлений + Копировать адрес ссылки Копировать ссылку Ссылка скопирована @@ -423,7 +427,6 @@ Выполняется загрузка изображения... Ошибка при загрузке изображения Повторить - Поиск Поиск заданий Поиск объявлений @@ -431,11 +434,35 @@ Поиск страниц Поиск тестов Нет совпадений по элементам \"%s\" + Выбрано Уведомления об обновлениях приложения Уведомления Canvas для обновлений в приложении. Приложение готово к обновлению Перезапустите приложение, чтобы установить новую версию + Пока ничего не запланировано + С оценкой + Ответы + Оценка + Поздно + Повторить + По уважительной причине + Завтра + Вчера + В %s + Задачи: %s + Срок: %s + Показать %d отсутствующие элементы + Скрыть %d отсутствующие элементы + Не удалось получить ваше расписание + Задачи + + %s баллов + %s баллов + %s баллов + %s баллов + %s баллов + diff --git a/libs/pandautils/src/main/res/values-sl/strings.xml b/libs/pandautils/src/main/res/values-sl/strings.xml index 2d1647268d..9143a2b154 100644 --- a/libs/pandautils/src/main/res/values-sl/strings.xml +++ b/libs/pandautils/src/main/res/values-sl/strings.xml @@ -17,7 +17,6 @@ --> - Izberi medij Posnemi videoposnetek Ni fotoaparata. @@ -42,8 +41,11 @@ Obvestila o nalaganju Obvestila sistema Canvas za nalaganja v teku. + + Dodaj datoteko + Fotoaparat Galerija Naprava @@ -96,6 +98,7 @@ Nalagam oddaje za %s Oddaja za %s ni uspela %s uspešno poslana + Prišlo je do nepričakovane napake. Prišlo je do napake strežnika. @@ -182,9 +185,9 @@ Zdi se, da ni ustvarjen še noben modul. Ni strani. Zdi se, da ni ustvarjene še nobene strani. - Za ta predmet so bili nekateri moduli onemogočeni. Prišlo je do težav + Da Ne @@ -325,6 +328,7 @@ + Datum sprostitve zapore ne more biti po roku. Datum zapore objave ne more biti pred rokom. Datum zapore objave ne more biti pred datumom sprostitve zapore. @@ -378,13 +382,14 @@ Naloži v komentar oddaje Dodaj datoteko s/z + Sporočila sistema Canvas Splošna sporočila sistema Canvas Dodatno konfiguriranje sporočil je mogoče v sekciji sistema Canvas »Prednostne nastavitve«. - %d novih obvestil + Kopiraj naslov povezave Kopiraj povezavo Povezava je kopirana. @@ -428,5 +433,26 @@ Obvestila Canvas za posodobitve v aplikaciji. Aplikacija pripravljena za posodobitev Aplikacijo zaženite znova, da namestite novo različico + Nič še ni načrtovano + Ocenjeno + Odgovori + Povratne informacije + Zamuda + Uveljavi + Opravičeno + Jutri + Včeraj + Ob %s + Čakajoča opravila: %s + Rok: %s + Prikaži %d manjkajočih elementov + Skrij %d manjkajočih elementov + Vašega urnika ni mogoče pridobiti + Čakajoča opravila + + %s točka + %s točka + %s točk + diff --git a/libs/pandautils/src/main/res/values-sv/strings.xml b/libs/pandautils/src/main/res/values-sv/strings.xml index 1b5599bac9..1372433e3e 100644 --- a/libs/pandautils/src/main/res/values-sv/strings.xml +++ b/libs/pandautils/src/main/res/values-sv/strings.xml @@ -17,7 +17,6 @@ --> - Välj media Spela in video Ingen kamera @@ -46,6 +45,7 @@ Lägg till fil + Kamera Galleri Enhet @@ -91,7 +91,7 @@ Filikon Laddar upp %d av %d %s har låsts. - Filerna är nu redo att ladda ned. Försök igen. + Filen är nu klar att laddas ner. Försök igen. Filen kan inte laddas ned utan att ge behörighet. Behörighet beviljades. Försök igen.. Välj en fil @@ -121,6 +121,7 @@ En eller flera filer har ett filtillägg som inte är tillåtet Den valda filtypen är inte tillåten. Tillåtna tillägg:  + Kontrollera din dataanslutning och försök igen. Din enhet har inte några program installerade som kan hantera den här filen. @@ -184,9 +185,9 @@ Det verkar inte som om några moduler har skapats än. Inga sidor Det verkar inte som om några sidor har skapats än. - Moduler har inaktiverats för den här kursen. Något gick fel + Ja Nej @@ -327,6 +328,7 @@ + Upplåsningsdatumet kan inte vara efter sista inlämningsdatumet Låsdatumet kan inte vara före inlämningsdatumet Låsdatumet kan inte vara före upplåsningsdatumet @@ -343,7 +345,7 @@ Lägg till ny - Ladda ned bilaga + Ladda ner bilaga Ta bort bilaga @@ -384,10 +386,10 @@ Canvas-avisering Allmänna Canvas-aviseringar Ytterligare konfiguration av aviseringar kan göras i sektionen för inställningar för Canvas-aviseringar. - %d nya aviseringar + Kopiera länkadress Kopiera länk Länken har kopierats @@ -431,5 +433,26 @@ Canvas-aviseringar för uppdateringar i appen. Appen är klar för uppdatering Starta om appen för att installera den nya versionen + Inget planerat ännu + Har bedömts + Svar + Feedback + Sen + Gör om + Ursäktad + I morgon + I går + kl. %s + Att göra: %s + Förfallodatum: %s + Visa %d saknade objekt + Dölj %d saknade objekt + Det gick inte att hämta ditt schema + Att göra + + %s poäng + %s poäng + %s poäng + diff --git a/libs/pandautils/src/main/res/values-th/strings.xml b/libs/pandautils/src/main/res/values-th/strings.xml index ac57f40d89..a5c77e8fd3 100644 --- a/libs/pandautils/src/main/res/values-th/strings.xml +++ b/libs/pandautils/src/main/res/values-th/strings.xml @@ -17,7 +17,6 @@ --> - เลือกไฟล์สื่อ ถ่ายวิดีโอ ไม่มีกล้อง @@ -329,6 +328,7 @@ + วันที่ปลดล็อคจะอยู่หลังจากวันครบกำหนดไม่ได้ วันที่ล็อคจะอยู่ก่อนวันครบกำหนดไม่ได้ วันที่ล็อคจะอยู่ก่อนวันที่ปลดล็อคไม่ได้ @@ -434,5 +434,26 @@ การแจ้งข้อมูลจาก Canvas สำหรับข้อมูลอัพเดตในแอพ แอพพร้อมสำหรับการอัพเดต รีสตาร์ทแอพเพื่อติดตั้งเวอร์ชั่นใหม่ + ยังไม่มีแผนตอนนี้ + ให้เกรดแล้ว + การตอบกลับ + ผลตอบรับ + ล่าช้า + ทำซ้ำ + ได้รับการยกเว้น + พรุ่งนี้ + เมื่อวานนี้ + เมื่อ %s + สิ่งที่ต้องทำ: %s + ครบกำหนด: %s + แสดง %d รายการที่ขาดหาย + ซ่อน %d รายการที่ขาดหาย + ไม่สามารถสืบค้นกำหนดเวลาของคุณได้ + สิ่งที่ต้องทำ + + %s คะแนน + %s คะแนน + %s คะแนน + diff --git a/libs/pandautils/src/main/res/values-zh-rHK/strings.xml b/libs/pandautils/src/main/res/values-zh-rHK/strings.xml index a195b93cb4..499bc22a3b 100644 --- a/libs/pandautils/src/main/res/values-zh-rHK/strings.xml +++ b/libs/pandautils/src/main/res/values-zh-rHK/strings.xml @@ -17,7 +17,6 @@ --> - 選擇媒體 錄製視頻 無攝像頭 @@ -42,8 +41,11 @@ 上傳通知 持續上傳的 Canvas 通知。 + + 添加檔案 + 攝像頭 圖片庫 設備 @@ -53,7 +55,6 @@ 檔案 檔案 %s 資料夾 %s - 添加項目 作業 附加的檔案 @@ -97,6 +98,7 @@ 上傳 %s 的提交項目 %s 的提交項目提交失敗 成功提交 %s + 意外錯誤出現。 伺服器發生錯誤。 @@ -183,9 +185,9 @@ 看來尚未建立任何單元。 無頁面 看來尚未建立任何頁面。 - 已停用此課程的單元。 發生問題 + @@ -323,6 +325,7 @@ + 解除鎖定日期不能在截止日期之後 鎖定日期不能在截止日期之前 鎖定日期不能在解鎖日期之前 @@ -376,13 +379,14 @@ 上傳到提交項目評論 附加檔案 + Canvas 通知 一般 Canvas 通知 可於 Canvas 通知偏好的部分進行更多通知配置。 - %d 個新通知 + 複製連結地址 複製連結 已複製連結 @@ -412,7 +416,6 @@ 正在上傳圖像... 圖像上傳錯誤 重試 - 搜尋 搜尋作業 搜尋通告 @@ -420,11 +423,32 @@ 搜尋頁面 搜尋測驗 沒有吻合的項目 \"%s\" + 已選擇 應用程式更新通知 Canvas 的應用程式內更新通知 應用程式準備好更新 重新啟動應用程式以安裝新版本 + 尚未計劃 + 已評分 + 回覆 + 反饋 + 逾期 + 重做 + 已免除 + 明天 + 昨天 + 時間 %s + 待辦事項:%s + 截止日期:%s + 顯示 %d 個缺少的項目 + 隱藏 %d 個缺少的項目 + 無法取得您的排程 + 待辦事項 + + %s 分 + %s 分 + diff --git a/libs/pandautils/src/main/res/values-zh/strings.xml b/libs/pandautils/src/main/res/values-zh/strings.xml index 77c5a3e885..2205760df9 100644 --- a/libs/pandautils/src/main/res/values-zh/strings.xml +++ b/libs/pandautils/src/main/res/values-zh/strings.xml @@ -17,7 +17,6 @@ --> - 选择媒体 录制视频 无摄像头 @@ -42,8 +41,11 @@ 上传通知 Canvas关于正在上传项的通知。 + + 添加文件 + 摄像机、照相机 画廊 设备 @@ -53,7 +55,6 @@ 文件 文件%s 文件夹%s - 添加项目 作业 附加文件 @@ -97,6 +98,7 @@ 正在为%s上传提交项 为%s提交失败 已成功提交%s + 发生意外错误。 发生服务器错误。 @@ -183,9 +185,9 @@ 似乎没有创建任何单元。 无页面 似乎没有创建任何页面。 - 已对此课程禁用模块。 遇到错误 + @@ -323,6 +325,7 @@ + 解锁时间不能晚于截止日期 锁定时间不能早于截止时间 锁定时间不能早于解锁时间 @@ -376,13 +379,14 @@ 上传到提交项评论 附加文件 + Canvas 通知 常规 Canvas 通知 在 Canvas 通知首选项部分可对通知进行进一步配置。 - %d 条新通知 + 复制链接地址 复制链接 已复制链接 @@ -412,7 +416,6 @@ 正在上传图像... 图像上传错误 重试 - 搜索 搜索作业 搜索公告 @@ -420,11 +423,32 @@ 搜索页面 搜索测验 无项目匹配\"%s\" + 已选择 应用程序更新通知 Canvas应用内更新通知。 可更新的应用程序 重启应用程序以安装新版本 + 暂无任何计划 + 已评分 + 回复 + 反馈 + 迟交 + 重新操作 + 已免除 + 明天 + 昨天 + %s + 待办事项:%s + 到期:%s + 显示 %d 个缺失的项目 + 隐藏 %d 个缺失的项目 + 无法获取您的日程计划 + 待办事项 + + %s 分 + %s 分 + diff --git a/libs/pandautils/src/main/res/values/dimens.xml b/libs/pandautils/src/main/res/values/dimens.xml index 89e1826442..89f5c3a986 100644 --- a/libs/pandautils/src/main/res/values/dimens.xml +++ b/libs/pandautils/src/main/res/values/dimens.xml @@ -48,5 +48,7 @@ 24dp 8dp - 360dp + 320dp + + 28dp diff --git a/libs/pandautils/src/main/res/values/strings.xml b/libs/pandautils/src/main/res/values/strings.xml index 04a1d6f31c..2abcff0827 100644 --- a/libs/pandautils/src/main/res/values/strings.xml +++ b/libs/pandautils/src/main/res/values/strings.xml @@ -434,5 +434,53 @@ Canvas notifications for in-app updates. App ready to update Restart the app to install the new version + Nothing planned yet + Graded + + %d Reply + %d Replies> + + Feedback + Late + Redo + Excused + Tomorrow + Yesterday + At %s + %1$s to %2$s + To Do: %s + Due: %s + Show %d missing items + Hide %d missing items + Unable to fetch your schedule + To Do + + %s pt + %s pt + %s pts + + + %.1f points + %.1f point + %.1f points + + All Day + Missing + Previous Week + Next Week + %1$s, %2$s + Course %s + Announcement + Discussion + Calendar event + Assignment + Planner note + Quiz + To Do + Page + Marked as done + Not marked as done + Mark as done + mark_as_not_done diff --git a/libs/pandautils/src/main/res/values/styles.xml b/libs/pandautils/src/main/res/values/styles.xml index bf3a13abbc..4edd39a8ab 100644 --- a/libs/pandautils/src/main/res/values/styles.xml +++ b/libs/pandautils/src/main/res/values/styles.xml @@ -204,4 +204,9 @@ center_vertical + + diff --git a/libs/pandautils/src/main/res/values/themes_canvasthemes.xml b/libs/pandautils/src/main/res/values/themes_canvasthemes.xml index 2f3f31d860..5f187373e6 100644 --- a/libs/pandautils/src/main/res/values/themes_canvasthemes.xml +++ b/libs/pandautils/src/main/res/values/themes_canvasthemes.xml @@ -59,4 +59,19 @@ @android:style/Animation + + + + diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/elementary/grades/GradesViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/elementary/grades/GradesViewModelTest.kt new file mode 100644 index 0000000000..baebb2a4f3 --- /dev/null +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/elementary/grades/GradesViewModelTest.kt @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.elementary.grades + +import android.content.res.Resources +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import com.instructure.canvasapi2.managers.CourseManager +import com.instructure.canvasapi2.managers.EnrollmentManager +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.models.GradingPeriod +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.grades.itemviewmodels.GradeRowItemViewModel +import com.instructure.pandautils.features.elementary.grades.itemviewmodels.GradingPeriodSelectorItemViewModel +import com.instructure.pandautils.mvvm.ViewState +import com.instructure.pandautils.utils.ColorApiHelper +import io.mockk.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import com.instructure.pandautils.features.elementary.grades.GradingPeriod as GradingPeriodView + +@ExperimentalCoroutinesApi +class GradesViewModelTest { + + @get:Rule + var instantExecutorRule = InstantTaskExecutorRule() + + private val lifecycleOwner: LifecycleOwner = mockk(relaxed = true) + private val lifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + private val testDispatcher = TestCoroutineDispatcher() + + private val courseManager: CourseManager = mockk(relaxed = true) + private val resources: Resources = mockk(relaxed = true) + private val enrollmentManager: EnrollmentManager = mockk(relaxed = true) + + private lateinit var viewModel: GradesViewModel + + @Before + fun setUp() { + every { resources.getString(R.string.currentGradingPeriod) } returns "Current" + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + Dispatchers.setMain(testDispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + testDispatcher.cleanupTestCoroutines() + } + + @Test + fun `Show error state if fetching courses fails`() { + // Given + every { resources.getString(R.string.failedToLoadGrades) } returns "Failed to load grades" + every { courseManager.getCoursesWithGradesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Fail() + } + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + // Then + assertTrue(viewModel.state.value is ViewState.Error) + assertEquals("Failed to load grades", (viewModel.state.value as ViewState.Error).errorMessage) + } + + @Test + fun `Show empty state if there are no courses`() { + // Given + every { courseManager.getCoursesWithGradesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(emptyList()) + } + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + // Then + assertTrue(viewModel.state.value is ViewState.Empty) + assertEquals(R.string.noGradesToDisplay, (viewModel.state.value as ViewState.Empty).emptyTitle) + } + + @Test + fun `Show success state and grades with correct data without grading periods if there are no grading periods`() { + // Given + val course1 = createCourseWithGrades(1, "Course with Grade", "", "www.1.com", 90.0, "A") + val course2 = createCourseWithGrades(2, "Course with Score", "#123456", "www.1.com", 75.6, "") + val course3 = createCourseWithGrades(3, "Course without scores", "#456789", "www.1.com", null, null) + val course4 = createCourseWithGrades(4, "Hide Final Grades", "#456789", "www.1.com", 50.0, "C", hideFinalGrades = true) + + every { courseManager.getCoursesWithGradesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course1, course2, course3, course4)) + } + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + // Then + assertTrue(viewModel.state.value is ViewState.Success) + assertEquals(4, viewModel.data.value!!.items.size) // We only expect 4 items here, because we don't have the grading period selector + + val gradeRows = viewModel.data.value!!.items.map { it as GradeRowItemViewModel } + + val expectedGradeRow1 = GradeRowViewData(1, "Course with Grade", ColorApiHelper.K5_DEFAULT_COLOR, "www.1.com", 90.0, "A") + val expectedGradeRow2 = GradeRowViewData(2, "Course with Score", "#123456", "www.1.com", 75.6, "76%") + val expectedGradeRow3 = GradeRowViewData(3, "Course without scores", "#456789", "www.1.com", null, "--") + val expectedGradeRow4 = GradeRowViewData(4, "Hide Final Grades", "#456789", "www.1.com", 0.0, "--") + + assertEquals(expectedGradeRow1, gradeRows[0].data) + assertEquals(expectedGradeRow2, gradeRows[1].data) + assertEquals(expectedGradeRow3, gradeRows[2].data) + assertEquals(expectedGradeRow4, gradeRows[3].data) + + assertEquals(0.9f, gradeRows[0].percentage) + assertEquals(0.756f, gradeRows[1].percentage) + assertEquals(0.0f, gradeRows[2].percentage) + assertEquals(0.0f, gradeRows[3].percentage) + } + + @Test + fun `Show success state and grades with correct data with grading periods`() { + // Given + val gradingPeriods = listOf(GradingPeriod(11, "Period 11"), GradingPeriod(12, "Period 12")) + val course1 = createCourseWithGrades(1, "Course with Grade", "", "www.1.com", 90.0, "A", gradingPeriods) + + every { courseManager.getCoursesWithGradesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course1)) + } + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + // Then + assertTrue(viewModel.state.value is ViewState.Success) + assertEquals(2, viewModel.data.value!!.items.size) // We have 1 item for grading period selector and 1 course item + + val gradeRows = viewModel.data.value!!.items + assertTrue(gradeRows[0] is GradingPeriodSelectorItemViewModel) + assertTrue(gradeRows[1] is GradeRowItemViewModel) + + val gradingPeriodsViewModel = gradeRows[0] as GradingPeriodSelectorItemViewModel + assertTrue(gradingPeriodsViewModel.isNotEmpty()) + assertEquals(GradingPeriodView(-1, "Current"), gradingPeriodsViewModel.selectedGradingPeriod) + } + + @Test + fun `Refresh error for current grading period sends refresh error event`() { + // Given + val course1 = createCourseWithGrades(1, "Course with Grade", "", "www.1.com", 90.0, "A") + + every { courseManager.getCoursesWithGradesAsync(any()) } returns mockk { + coEvery { await() } returnsMany listOf(DataResult.Success(listOf(course1)), DataResult.Fail()) + } + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + viewModel.refresh() + + // Then + assertEquals(GradesAction.ShowRefreshError, viewModel.events.value!!.getContentIfNotHandled()!!) + assertEquals(ViewState.Error(), viewModel.state.value!!) + } + + @Test + fun `Do nothing when the same grading period is selected`() { + // Given + val gradingPeriods = listOf(GradingPeriod(11, "Period 11"), GradingPeriod(12, "Period 12")) + val course1 = createCourseWithGrades(1, "Course with Grade", "", "www.1.com", 90.0, "A", gradingPeriods) + + every { courseManager.getCoursesWithGradesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course1)) + } + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + clearMocks(courseManager, enrollmentManager) + + viewModel.gradingPeriodSelected(GradingPeriodView(-1, "Current")) + + // Then + verify(exactly = 0) { courseManager.getCoursesWithGradesAsync(any()) } + verify(exactly = 0) { enrollmentManager.getEnrollmentsForGradingPeriodAsync(any(), any()) } + } + + @Test + fun `Load grades for grading period if different grading period is selected`() { + // Given + val gradingPeriods = listOf(GradingPeriod(11, "Period 11"), GradingPeriod(12, "Period 12")) + val course1 = createCourseWithGrades(1, "Course with Grade", "", "www.1.com", 90.0, "A", gradingPeriods) + + every { courseManager.getCoursesWithGradesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course1)) + } + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + clearMocks(courseManager, enrollmentManager) + + viewModel.gradingPeriodSelected(GradingPeriodView(11, "Period 11")) + + // Then + verify(exactly = 0) { courseManager.getCoursesWithGradesAsync(any()) } + verify(exactly = 1) { enrollmentManager.getEnrollmentsForGradingPeriodAsync(eq(11), any()) } + } + + @Test + fun `Reselect previous grading period if there is an error loading grades for grading period`() { + // Given + val gradingPeriods = listOf(GradingPeriod(11, "Period 11"), GradingPeriod(12, "Period 12")) + val course1 = createCourseWithGrades(1, "Course with Grade", "", "www.1.com", 90.0, "A", gradingPeriods) + + every { courseManager.getCoursesWithGradesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course1)) + } + + every { enrollmentManager.getEnrollmentsForGradingPeriodAsync(any(), any()) } returns mockk() { + coEvery { await() } returns DataResult.Fail() + } + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + clearMocks(courseManager, enrollmentManager) + + viewModel.gradingPeriodSelected(GradingPeriodView(11, "Period 11")) + + // Then + assertEquals(GradingPeriodView(-1, "Current"), (viewModel.data.value!!.items[0] as GradingPeriodSelectorItemViewModel).selectedGradingPeriod) + } + + @Test + fun `Load grades for course is current grading period is reselected`() { + // Given + val gradingPeriods = listOf(GradingPeriod(11, "Period 11"), GradingPeriod(12, "Period 12")) + val course1 = createCourseWithGrades(1, "Course with Grade", "", "www.1.com", 90.0, "A", gradingPeriods) + + every { courseManager.getCoursesWithGradesAsync(any()) } returns mockk { + coEvery { await() } returnsMany listOf(DataResult.Success(listOf(course1)), DataResult.Fail()) + } + + every { enrollmentManager.getEnrollmentsForGradingPeriodAsync(any(), any()) } returns mockk() { + coEvery { await() } returns DataResult.Success(emptyList()) + } + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + viewModel.gradingPeriodSelected(GradingPeriodView(11, "Period 11")) + + clearMocks(courseManager, enrollmentManager) + + viewModel.gradingPeriodSelected(GradingPeriodView(-1, "Current")) + + // Then + verify(exactly = 1) { courseManager.getCoursesWithGradesAsync(any()) } + verify(exactly = 0) { enrollmentManager.getEnrollmentsForGradingPeriodAsync(eq(11), any()) } + } + + @Test + fun `Open grades page whe grades row clicked`() { + // Given + val course1 = createCourseWithGrades(1, "Course with Grade", "", "www.1.com", 90.0, "A") + + every { courseManager.getCoursesWithGradesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course1)) + } + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + val gradeRowViewModel = viewModel.data.value!!.items[0] as GradeRowItemViewModel + gradeRowViewModel.onRowClicked() + + // Then + assertEquals(GradesAction.OpenCourseGrades(course1), viewModel.events.value!!.getContentIfNotHandled()) + } + + @Test + fun `Show grading period selector dialog if grading period is clicked`() { + // Given + val gradingPeriods = listOf(GradingPeriod(11, "Period 11"), GradingPeriod(12, "Period 12")) + val course1 = createCourseWithGrades(1, "Course with Grade", "", "www.1.com", 90.0, "A", gradingPeriods) + + every { courseManager.getCoursesWithGradesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course1)) + } + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + val gradingPeriodsViewModel = viewModel.data.value!!.items[0] as GradingPeriodSelectorItemViewModel + gradingPeriodsViewModel.onClick() + + // Then + val expectedGradingPeriods = listOf( + GradingPeriodView(-1, "Current"), + GradingPeriodView(11, "Period 11"), + GradingPeriodView(12, "Period 12") + ) + assertEquals(GradesAction.OpenGradingPeriodsDialog(expectedGradingPeriods, 0), viewModel.events.value!!.getContentIfNotHandled()) + } + + private fun createViewModel() = GradesViewModel(courseManager, resources, enrollmentManager) + + private fun createCourseWithGrades( + id: Long, + name: String, + color: String, + imageUrl: String, + score: Double?, + grade: String?, + gradingPeriods: List? = null, + hideFinalGrades: Boolean = false + ): Course { + val enrollment = Enrollment(id = 123, computedCurrentScore = score, computedCurrentGrade = grade) + return Course( + id = id, + name = name, + courseColor = color, + imageUrl = imageUrl, + enrollments = mutableListOf(enrollment), + gradingPeriods = gradingPeriods, + hideFinalGrades = hideFinalGrades) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/elementary/homeroom/CourseCardCreatorTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/elementary/homeroom/CourseCardCreatorTest.kt index fa9b1c55d4..3e3c665418 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/elementary/homeroom/CourseCardCreatorTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/elementary/homeroom/CourseCardCreatorTest.kt @@ -131,13 +131,13 @@ class CourseCardCreatorTest { val courses = listOf(Course(id = 1), Course(id = 2)) val plannerItems = listOf( - createPlannerItem(1, "assignment", false, false), - createPlannerItem(1, "todo", false, false), - createPlannerItem(1, "assignment", false, true), - createPlannerItem(1, "assignment", true, false), - createPlannerItem(2, "assignment", false, false), - createPlannerItem(2, "assignment", false, false), - createPlannerItem(3, "assignment", false, false), + createPlannerItem(1, PlannableType.ASSIGNMENT, false, false), + createPlannerItem(1, PlannableType.TODO, false, false), + createPlannerItem(1, PlannableType.ASSIGNMENT, false, true), + createPlannerItem(1, PlannableType.ASSIGNMENT, true, false), + createPlannerItem(2, PlannableType.ASSIGNMENT, false, false), + createPlannerItem(2, PlannableType.ASSIGNMENT, false, false), + createPlannerItem(3, PlannableType.ASSIGNMENT, false, false), ) val announcementsDeferred: Deferred>> = mockk() @@ -165,10 +165,10 @@ class CourseCardCreatorTest { val courses = listOf(Course(id = 1)) val plannerItems = listOf( - createPlannerItem(1, "todo", false, false), - createPlannerItem(1, "assignment", false, true), - createPlannerItem(1, "assignment", true, false), - createPlannerItem(2, "assignment", false, false), + createPlannerItem(1, PlannableType.TODO, false, false), + createPlannerItem(1, PlannableType.ASSIGNMENT, false, true), + createPlannerItem(1, PlannableType.ASSIGNMENT, true, false), + createPlannerItem(2, PlannableType.ASSIGNMENT, false, false), ) every { plannerManager.getPlannerItemsAsync(any(), any(), any()) } returns mockk { @@ -193,8 +193,8 @@ class CourseCardCreatorTest { val assignments = listOf( Assignment(id = 1, courseId = 1), Assignment(id = 2, courseId = 2), - Assignment(id = 3, courseId = 1, plannerOverride = PlannerOverride(false)), - Assignment(id = 4, courseId = 1, plannerOverride = PlannerOverride(true))) + Assignment(id = 3, courseId = 1, plannerOverride = PlannerOverride(plannableId = 3, plannableType = PlannableType.ASSIGNMENT, dismissed = false)), + Assignment(id = 4, courseId = 1, plannerOverride = PlannerOverride(plannableId = 4, plannableType = PlannableType.ASSIGNMENT, dismissed = true))) every { userManager.getAllMissingSubmissionsAsync(any()) } returns mockk { coEvery { await() } returns DataResult.Success(assignments) } @@ -216,7 +216,7 @@ class CourseCardCreatorTest { val assignments = listOf( Assignment(id = 1, courseId = 2), - Assignment(id = 4, courseId = 1, plannerOverride = PlannerOverride(true))) + Assignment(id = 4, courseId = 1, plannerOverride = PlannerOverride(plannableId = 4, plannableType = PlannableType.ASSIGNMENT, dismissed = true))) every { userManager.getAllMissingSubmissionsAsync(any()) } returns mockk { coEvery { await() } returns DataResult.Success(assignments) } @@ -242,8 +242,8 @@ class CourseCardCreatorTest { assertEquals("#394B58", courseCards[0].data.courseColor) } - private fun createPlannerItem(courseId: Long, plannableType: String, submitted: Boolean, missing: Boolean): PlannerItem { + private fun createPlannerItem(courseId: Long, plannableType: PlannableType, submitted: Boolean, missing: Boolean): PlannerItem { val plannable = mockk() - return PlannerItem(courseId, null, null, null, null, plannableType, plannable, Date(), null, SubmissionState(submitted, missing)) + return PlannerItem(courseId, null, null, null, null, plannableType, plannable, Date(), null, SubmissionState(submitted, missing), false) } } \ No newline at end of file diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/elementary/resources/ResourcesViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/elementary/resources/ResourcesViewModelTest.kt new file mode 100644 index 0000000000..971ad12804 --- /dev/null +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/elementary/resources/ResourcesViewModelTest.kt @@ -0,0 +1,388 @@ +package com.instructure.pandautils.features.elementary.resources/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +import android.content.res.Resources +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import com.instructure.canvasapi2.managers.CourseManager +import com.instructure.canvasapi2.managers.ExternalToolManager +import com.instructure.canvasapi2.managers.OAuthManager +import com.instructure.canvasapi2.managers.UserManager +import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.resources.itemviewmodels.ContactInfoItemViewModel +import com.instructure.pandautils.features.elementary.resources.itemviewmodels.ImportantLinksItemViewModel +import com.instructure.pandautils.features.elementary.resources.itemviewmodels.LtiApplicationItemViewModel +import com.instructure.pandautils.features.elementary.resources.itemviewmodels.ResourcesHeaderViewModel +import com.instructure.pandautils.mvvm.ViewState +import com.instructure.pandautils.utils.HtmlContentFormatter +import io.mockk.* +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@ExperimentalCoroutinesApi +class ResourcesViewModelTest { + + @get:Rule + var instantExecutorRule = InstantTaskExecutorRule() + + private val lifecycleOwner: LifecycleOwner = mockk(relaxed = true) + private val lifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + private val testDispatcher = TestCoroutineDispatcher() + + private val resources: Resources = mockk(relaxed = true) + private val courseManager: CourseManager = mockk(relaxed = true) + private val userManager: UserManager = mockk(relaxed = true) + private val externalToolManager: ExternalToolManager = mockk(relaxed = true) + private val oAuthManager: OAuthManager = mockk(relaxed = true) + private val htmlContentFormatter: HtmlContentFormatter = mockk(relaxed = true) + + private lateinit var viewModel: ResourcesViewModel + + @Before + fun setUp() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + Dispatchers.setMain(testDispatcher) + coEvery { htmlContentFormatter.formatHtmlWithIframes(any()) } returnsArgument 0 + + mockkStatic("kotlinx.coroutines.AwaitKt") + } + + @After + fun tearDown() { + Dispatchers.resetMain() + testDispatcher.cleanupTestCoroutines() + } + + @Test + fun `Show error state if fetching courses fails`() { + // Given + every { resources.getString(R.string.failedToLoadResources) } returns "Error" + initMockData(courses = DataResult.Fail()) + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + // Then + assertTrue(viewModel.state.value is ViewState.Error) + assertEquals("Error", (viewModel.state.value as ViewState.Error).errorMessage) + } + + @Test + fun `Show empty state if there are no courses`() { + // Given + initMockData() + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + // Then + assertTrue(viewModel.state.value is ViewState.Empty) + assertEquals(R.string.resourcesEmptyMessage, (viewModel.state.value as ViewState.Empty).emptyTitle) + } + + @Test + fun `Do not create important links when we have a course but syllabus is empty`() { + // Given + val course = Course(syllabusBody = "") + initMockData(courses = DataResult.Success(listOf(course))) + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + // Then + assertTrue(viewModel.state.value is ViewState.Empty) + assertEquals(R.string.resourcesEmptyMessage, (viewModel.state.value as ViewState.Empty).emptyTitle) + } + + @Test + fun `Create important links from homeroom course syllabus body without course name if only 1 homeroom course is present`() { + // Given + val course = Course(homeroomCourse = true, syllabusBody = "This link is really important: www.tamaskozmer.com") + initMockData(courses = DataResult.Success(listOf(course))) + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + // Then + assertTrue(viewModel.state.value is ViewState.Success) + assertEquals(1, viewModel.data.value!!.importantLinksItems.size) + + val expectedHtmlContent = "This link is really important: www.tamaskozmer.com" + assertFalse(viewModel.data.value!!.isEmpty()) + val viewData = (viewModel.data.value!!.importantLinksItems[0] as ImportantLinksItemViewModel).data + assertEquals(ImportantLinksViewData("", expectedHtmlContent), viewData) + } + + @Test + fun `Create important links from homeroom course syllabus body with course name if there are more than 1 homeroom courses`() { + // Given + val course = Course(name = "Course 1", homeroomCourse = true, syllabusBody = "This link is really important: www.tamaskozmer.com") + val course2 = Course(name = "Course 2", homeroomCourse = true, syllabusBody = "Something really important") + initMockData(courses = DataResult.Success(listOf(course, course2))) + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + // Then + assertTrue(viewModel.state.value is ViewState.Success) + assertEquals(2, viewModel.data.value!!.importantLinksItems.size) + assertFalse(viewModel.data.value!!.isEmpty()) + + val viewData1 = (viewModel.data.value!!.importantLinksItems[0] as ImportantLinksItemViewModel).data + val viewData2 = (viewModel.data.value!!.importantLinksItems[1] as ImportantLinksItemViewModel).data + assertEquals(ImportantLinksViewData("Course 1", "This link is really important: www.tamaskozmer.com", true), viewData1) + assertEquals(ImportantLinksViewData("Course 2", "Something really important"), viewData2) + } + + @Test + fun `Do not create important links from non-homeroom course syllabus body`() { + // Given + val course = Course(syllabusBody = "This is a syllabus, not important links") + initMockData(courses = DataResult.Success(listOf(course))) + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + // Then + assertTrue(viewModel.state.value is ViewState.Empty) + assertEquals(R.string.resourcesEmptyMessage, (viewModel.state.value as ViewState.Empty).emptyTitle) + } + + @Test + fun `Do not request lti tools if there are no non-homeroom courses`() { + // Given + val course = Course(homeroomCourse = true, syllabusBody = "This link is really important: www.tamaskozmer.com") + initMockData(courses = DataResult.Success(listOf(course))) + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + // Then + verify(exactly = 0) { externalToolManager.getExternalToolsForCoursesAsync(any(), any()) } + } + + @Test + fun `Do not create action items and headers if no external tools and staff info received`() { + // Given + val course = Course(id = 1, homeroomCourse = true, syllabusBody = "This link is really important: www.tamaskozmer.com") + val course2 = Course(id = 2, homeroomCourse = false) + initMockData(courses = DataResult.Success(listOf(course, course2))) + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + // Then + assertTrue(viewModel.data.value!!.actionItems.isEmpty()) + } + + @Test + fun `Create lti tools items from course lti tools and remove duplicated items`() { + // Given + val course = Course(id = 1, name = "Course uno") + val course2 = Course(id = 2, name = "Course due") + + val ltiTools = listOf( + LTITool(id = 1, contextId = course.id, contextName = course.name, courseNavigation = CourseNavigation("Google Drive"), url = "google.com", iconUrl = "drive.png"), + LTITool(id = 1, contextId = course2.id, contextName = course2.name, courseNavigation = CourseNavigation("Google Drive"), url = "google.com", iconUrl = "drive.png"), + LTITool(id = 2, name = "New Quizzes", contextId = course.id, contextName = course.name, url = "new.quizzes.com", iconUrl = "newquizzes.png") + ) + initMockData(courses = DataResult.Success(listOf(course, course2)), externalTools = DataResult.Success(ltiTools)) + every { resources.getString(R.string.studentApplications) } returns "Student Applications" + every { resources.getDimension(R.dimen.ltiAppsBottomMargin) } returns 10f + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + // Then + assertTrue(viewModel.state.value is ViewState.Success) + assertEquals(3, viewModel.data.value!!.actionItems.size) // We have 3 items, 2 LTI tools with the header + assertFalse(viewModel.data.value!!.isEmpty()) + + val header = viewModel.data.value!!.actionItems[0] as ResourcesHeaderViewModel + assertEquals(ResourcesHeaderViewData("Student Applications"), header.data) + + val ltiTool1 = viewModel.data.value!!.actionItems[1] as LtiApplicationItemViewModel + assertEquals(0, ltiTool1.marginBottom) + assertEquals(LtiApplicationViewData("Google Drive", "drive.png", "google.com"), ltiTool1.data) + + val ltiTool2 = viewModel.data.value!!.actionItems[2] as LtiApplicationItemViewModel + assertEquals(10, ltiTool2.marginBottom) + assertEquals(LtiApplicationViewData("New Quizzes", "newquizzes.png", "new.quizzes.com"), ltiTool2.data) + } + + @Test + fun `Lti app click opens Lti app dialog with the list of courses for that lti app`() { + // Given + val course = Course(id = 1, name = "Course uno") + val course2 = Course(id = 2, name = "Course due") + + val ltiTools = listOf( + LTITool(id = 1, contextId = course.id, contextName = course.name, courseNavigation = CourseNavigation("Google Drive"), url = "google.com", iconUrl = "drive.png"), + LTITool(id = 1, contextId = course2.id, contextName = course2.name, courseNavigation = CourseNavigation("Google Drive"), url = "google.com", iconUrl = "drive.png"), + LTITool(id = 2, name = "New Quizzes", contextId = course.id, contextName = course.name, url = "new.quizzes.com", iconUrl = "newquizzes.png") + ) + initMockData(courses = DataResult.Success(listOf(course, course2)), externalTools = DataResult.Success(ltiTools)) + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + val ltiTool = viewModel.data.value!!.actionItems[1] as LtiApplicationItemViewModel + ltiTool.onClick() + + // Then + val event = viewModel.events.value!!.getContentIfNotHandled()!! + assertTrue(event is ResourcesAction.OpenLtiApp) + + val expectedLtiToolList = listOf( + LTITool(id = 1, contextId = 1, contextName = "Course uno", courseNavigation = CourseNavigation("Google Drive"), url = "google.com", iconUrl = "drive.png"), + LTITool(id = 1, contextId = 2, contextName = "Course due", courseNavigation = CourseNavigation("Google Drive"), url = "google.com", iconUrl = "drive.png") + ) + assertEquals(expectedLtiToolList, (event as ResourcesAction.OpenLtiApp).ltiTools) + } + + @Test + fun `Create staff info views with header and remove duplicates`() { + // Given + val course = Course(id = 1, homeroomCourse = true, syllabusBody = "This link is really important: www.tamaskozmer.com") + val teachers1 = listOf( + User(id = 1, shortName = "Tamas Kozmer", avatarUrl = "http://a.b", enrollments = listOf(Enrollment(role = Enrollment.EnrollmentType.Teacher))), + User(id = 2, shortName = "Balint Bartok", avatarUrl = "http://b.c", enrollments = listOf(Enrollment(role = Enrollment.EnrollmentType.Ta))) + ) + val teachers2 = listOf(User(id = 1, shortName = "Tamas Kozmer", avatarUrl = "http://a.b", enrollments = listOf(Enrollment(role = Enrollment.EnrollmentType.Teacher)))) + initMockData(courses = DataResult.Success(listOf(course)), teachers = listOf(DataResult.Success(teachers1), DataResult.Success(teachers2))) + + every { resources.getString(R.string.staffContactInfo) } returns "Staff Info" + every { resources.getString(R.string.staffRoleTeacher) } returns "Teacher" + every { resources.getString(R.string.staffRoleTeacherAssistant) } returns "Assistant" + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + // Then + assertTrue(viewModel.state.value is ViewState.Success) + assertEquals(3, viewModel.data.value!!.actionItems.size) // We have 3 items, 2 staff info items with the header + assertFalse(viewModel.data.value!!.isEmpty()) + + val header = viewModel.data.value!!.actionItems[0] as ResourcesHeaderViewModel + assertEquals(ResourcesHeaderViewData("Staff Info", hasDivider = true), header.data) + + val contactInfo1 = viewModel.data.value!!.actionItems[1] as ContactInfoItemViewModel + assertEquals(ContactInfoViewData("Tamas Kozmer", "Teacher", "http://a.b"), contactInfo1.data) + + val contactInfo2 = viewModel.data.value!!.actionItems[2] as ContactInfoItemViewModel + assertEquals(ContactInfoViewData("Balint Bartok", "Assistant", "http://b.c"), contactInfo2.data) + } + + @Test + fun `Clicking contact info opens compose message`() { + // Given + val course = Course(id = 1, homeroomCourse = true, syllabusBody = "This link is really important: www.tamaskozmer.com") + val teachers = listOf(User(id = 1, shortName = "Tamas Kozmer", avatarUrl = "http://a.b", enrollments = listOf(Enrollment(role = Enrollment.EnrollmentType.Teacher)))) + initMockData(courses = DataResult.Success(listOf(course)), teachers = listOf(DataResult.Success(teachers))) + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + val contactInfo = viewModel.data.value!!.actionItems[1] as ContactInfoItemViewModel + contactInfo.onClick() + + // Then + val event = viewModel.events.value!!.getContentIfNotHandled()!! + assertTrue(event is ResourcesAction.OpenComposeMessage) + assertEquals(ResourcesAction.OpenComposeMessage(teachers[0]), event) + } + + @Test + fun `Error after refresh should trigger refresh error event if data is already available`() { + // Given + val course = Course(id = 1, homeroomCourse = true, syllabusBody = "This link is really important: www.tamaskozmer.com") + initMockData(courses = DataResult.Success(listOf(course))) + every { courseManager.getCoursesWithSyllabusAsyncWithActiveEnrollmentAsync(any()) } returns mockk { + coEvery { await() }.returnsMany(DataResult.Success(listOf(course)), DataResult.Fail()) + } + + // When + viewModel = createViewModel() + viewModel.state.observe(lifecycleOwner, {}) + + viewModel.refresh() + + // Then + assertEquals(ViewState.Error(), viewModel.state.value) + assertEquals(ResourcesAction.ShowRefreshError, viewModel.events.value!!.getContentIfNotHandled()!!) + } + + @Test + fun `OnImportantLinksViewsReady should send event`() { + // When + viewModel = createViewModel() + viewModel.events.observe(lifecycleOwner, {}) + viewModel.onImportantLinksViewsReady() + + // Then + assertEquals(ResourcesAction.ImportantLinksViewsReady, viewModel.events.value!!.getContentIfNotHandled()!!) + } + + private fun createViewModel(): ResourcesViewModel { + return ResourcesViewModel(resources, courseManager, userManager, externalToolManager, oAuthManager, htmlContentFormatter) + } + + private fun initMockData( + courses: DataResult> = DataResult.Success(emptyList()), + externalTools: DataResult> = DataResult.Success(emptyList()), + teachers: List>> = listOf(DataResult.Success(emptyList())) + ) { + every { courseManager.getCoursesWithSyllabusAsyncWithActiveEnrollmentAsync(any()) } returns mockk { + coEvery { await() } returns courses + } + every { externalToolManager.getExternalToolsForCoursesAsync(any(), any()) } returns mockk() { + coEvery { await() } returns externalTools + } + + val usersDeferred: Deferred>> = mockk() + every { userManager.getTeacherListForCourseAsync(any(), any()) } returns usersDeferred + val listOfUsersDeferred = courses.dataOrNull?.map { usersDeferred } ?: emptyList() + coEvery { listOfUsersDeferred.awaitAll() } returns teachers + } +} diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/elementary/schedule/ScheduleViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/elementary/schedule/ScheduleViewModelTest.kt new file mode 100644 index 0000000000..98fe312bc4 --- /dev/null +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/elementary/schedule/ScheduleViewModelTest.kt @@ -0,0 +1,929 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.features.elementary.schedule + +import android.content.res.Resources +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import com.instructure.canvasapi2.managers.* +import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.toApiString +import com.instructure.pandautils.R +import com.instructure.pandautils.features.elementary.schedule.itemviewmodels.* +import com.instructure.pandautils.utils.MissingItemsPrefs +import com.instructure.pandautils.utils.date.RealDateTimeProvider +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import junit.framework.Assert.assertEquals +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.setMain +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.* + +@ExperimentalCoroutinesApi +class ScheduleViewModelTest { + + @get:Rule + var instantExecutorRule = InstantTaskExecutorRule() + + private val lifecycleOwner: LifecycleOwner = mockk(relaxed = true) + private val lifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + private val testDispatcher = TestCoroutineDispatcher() + + private val plannerManager: PlannerManager = mockk(relaxed = true) + private val courseManager: CourseManager = mockk(relaxed = true) + private val userManager: UserManager = mockk(relaxed = true) + private val resources: Resources = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val calendarEventManager: CalendarEventManager = mockk(relaxed = true) + private val assignmentManager: AssignmentManager = mockk(relaxed = true) + private val missingItemsPrefs: MissingItemsPrefs = mockk(relaxed = true) + private val dateTimeProvider = RealDateTimeProvider() + + private lateinit var viewModel: ScheduleViewModel + + @Before + fun setUp() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + Dispatchers.setMain(testDispatcher) + + mockkStatic("kotlinx.coroutines.AwaitKt") + + setupStrings() + + every { courseManager.getCoursesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(emptyList()) + } + + every { userManager.getAllMissingSubmissionsAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(emptyList()) + } + + every { plannerManager.getPlannerItemsAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(emptyList()) + } + + every { missingItemsPrefs.itemsCollapsed } returns false + } + + @Test + fun `Open actions map correctly`() { + val course = Course(id = 1) + + every { courseManager.getCoursesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course)) + } + + val plannerItems = listOf( + createPlannerItem(courseId = course.id, assignmentId = 1, PlannableType.ASSIGNMENT, SubmissionState(submitted = true), Date()), + createPlannerItem(courseId = course.id, assignmentId = 2, PlannableType.QUIZ, SubmissionState(submitted = true), Date()), + createPlannerItem(courseId = course.id, assignmentId = 3, PlannableType.ANNOUNCEMENT, SubmissionState(submitted = true), Date()), + createPlannerItem(courseId = course.id, assignmentId = 4, PlannableType.DISCUSSION_TOPIC, SubmissionState(submitted = true), Date()), + ) + + every { plannerManager.getPlannerItemsAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(plannerItems) + } + + val assignments = listOf( + createAssignment(3, 1), + createAssignment(4, 1) + ).map { DataResult.Success(it) } + + mockAssignments(assignments) + + viewModel = createViewModel() + viewModel.getDataForDate(Date().toApiString()) + viewModel.data.observe(lifecycleOwner, {}) + + val items = viewModel.data.value?.itemViewModels + + val todayHeader = items?.find { it.dayText == "Today" } + assert(todayHeader is ScheduleDayGroupItemViewModel) + assertEquals("Today", todayHeader?.dayText) + + val courseItemViewModel = todayHeader?.items?.get(0) as ScheduleCourseItemViewModel + + courseItemViewModel.onHeaderClick.invoke() + assertEquals(ScheduleAction.OpenCourse(course), viewModel.events.value?.getContentIfNotHandled()) + + val assignment = courseItemViewModel.data.plannerItems.find { it.data.type == PlannerItemType.ASSIGNMENT } + assignment?.open?.invoke() + assertEquals( + ScheduleAction.OpenAssignment(plannerItems[0].canvasContext, plannerItems[0].plannable.id), + viewModel.events.value?.getContentIfNotHandled() + ) + + val quiz = courseItemViewModel.data.plannerItems.find { it.data.type == PlannerItemType.QUIZ } + quiz?.open?.invoke() + assertEquals( + ScheduleAction.OpenAssignment(plannerItems[1].canvasContext, plannerItems[1].plannable.id), + viewModel.events.value?.getContentIfNotHandled() + ) + + val announcement = courseItemViewModel.data.plannerItems.find { it.data.type == PlannerItemType.ANNOUNCEMENT } + announcement?.open?.invoke() + assertEquals( + ScheduleAction.OpenDiscussion( + plannerItems[2].canvasContext, + plannerItems[2].plannable.id, + plannerItems[2].plannable.title + ), viewModel.events.value?.getContentIfNotHandled() + ) + + val discussion = courseItemViewModel.data.plannerItems.find { it.data.type == PlannerItemType.DISCUSSION } + discussion?.open?.invoke() + assertEquals( + ScheduleAction.OpenDiscussion( + plannerItems[3].canvasContext, + plannerItems[3].plannable.id, + plannerItems[3].plannable.title + ), viewModel.events.value?.getContentIfNotHandled() + ) + } + + + @Test + fun `Missing items set up correctly`() { + val courses = listOf( + Course(id = 1, name = "Course 1"), + Course(id = 2, name = "Course 2") + ) + + every { courseManager.getCoursesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(courses) + } + + val missingItems = listOf( + createAssignment( + 1, + courseId = 1, + createSubmission(id = 1, grade = null, late = false, excused = false), + name = "Assignment 1" + ), + createAssignment( + 2, + courseId = 2, + createSubmission(id = 2, grade = null, late = false, excused = false), + name = "Assignment 2" + ) + ) + + every { userManager.getAllMissingSubmissionsAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(missingItems) + } + + viewModel = createViewModel() + viewModel.getDataForDate(Date().toApiString()) + viewModel.data.observe(lifecycleOwner, {}) + + val items = viewModel.data.value?.itemViewModels + + val todayHeader = items?.find { it.dayText == "Today" } + assert(todayHeader is ScheduleDayGroupItemViewModel) + assertEquals("Today", todayHeader?.dayText) + + val missingItemHeader = + todayHeader?.items?.find { it is ScheduleMissingItemsGroupItemViewModel } as ScheduleMissingItemsGroupItemViewModel + assertEquals(2, missingItemHeader.items.size) + + val firstMissingItem = missingItemHeader.items[0] as ScheduleMissingItemViewModel + assertEquals("Assignment 1", firstMissingItem.data.title) + assertEquals("Course 1", firstMissingItem.data.courseName) + + val secondMissingItem = missingItemHeader.items[1] as ScheduleMissingItemViewModel + assertEquals("Assignment 2", secondMissingItem.data.title) + assertEquals("Course 2", secondMissingItem.data.courseName) + } + + @Test + fun `Missing items are open by default`() { + val course = Course(id = 1) + + every { courseManager.getCoursesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course)) + } + + val missingItems = listOf( + createAssignment(1, courseId = 1, createSubmission(id = 1, grade = null, late = false, excused = false)), + createAssignment(2, courseId = 1, createSubmission(id = 2, grade = null, late = false, excused = false)) + ) + + every { userManager.getAllMissingSubmissionsAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(missingItems) + } + + viewModel = createViewModel() + viewModel.getDataForDate(Date().toApiString()) + viewModel.data.observe(lifecycleOwner, {}) + + val items = viewModel.data.value?.itemViewModels + + val todayHeader = items?.find { it.dayText == "Today" } + assert(todayHeader is ScheduleDayGroupItemViewModel) + assertEquals("Today", todayHeader?.dayText) + + val missingItemHeader = + todayHeader?.items?.find { it is ScheduleMissingItemsGroupItemViewModel } as ScheduleMissingItemsGroupItemViewModel + assertEquals(false, missingItemHeader.collapsed) + } + + @Test + fun `Missing item header changes state correctly`() { + val course = Course(id = 1) + + every { courseManager.getCoursesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course)) + } + + val missingItems = listOf( + createAssignment(1, courseId = 1, createSubmission(id = 1, grade = null, late = false, excused = false)), + createAssignment(2, courseId = 1, createSubmission(id = 2, grade = null, late = false, excused = false)) + ) + + every { userManager.getAllMissingSubmissionsAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(missingItems) + } + + viewModel = createViewModel() + viewModel.getDataForDate(Date().toApiString()) + viewModel.data.observe(lifecycleOwner, {}) + + val items = viewModel.data.value?.itemViewModels + + val todayHeader = items?.find { it.dayText == "Today" } + assert(todayHeader is ScheduleDayGroupItemViewModel) + assertEquals("Today", todayHeader?.dayText) + + val missingItemHeader = + todayHeader?.items?.find { it is ScheduleMissingItemsGroupItemViewModel } as ScheduleMissingItemsGroupItemViewModel + assertEquals(false, missingItemHeader.collapsed) + missingItemHeader.toggleItems() + assertEquals(true, missingItemHeader.collapsed) + } + + @Test + fun `Only one missing item header is found`() { + val course = Course(id = 1) + + every { courseManager.getCoursesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course)) + } + + val missingItems = listOf( + createAssignment(1, courseId = 1, createSubmission(id = 1, grade = null, late = false, excused = false)), + createAssignment(2, courseId = 1, createSubmission(id = 2, grade = null, late = false, excused = false)) + ) + + every { userManager.getAllMissingSubmissionsAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(missingItems) + } + + viewModel = createViewModel() + viewModel.getDataForDate(Date().toApiString()) + viewModel.data.observe(lifecycleOwner, {}) + + val items = viewModel.data.value?.itemViewModels + + val todayHeader = items?.find { it.dayText == "Today" } + assert(todayHeader is ScheduleDayGroupItemViewModel) + assertEquals("Today", todayHeader?.dayText) + + items?.forEach { dayGroup -> + if (dayGroup != todayHeader) { + assertEquals(0, dayGroup.items.count { it is ScheduleMissingItemsGroupItemViewModel }) + } else { + assertEquals(1, dayGroup.items.count { it is ScheduleMissingItemsGroupItemViewModel }) + } + } + } + + @Test + fun `Missing items are not visible if there are none`() { + viewModel = createViewModel() + viewModel.getDataForDate(Date().toApiString()) + viewModel.data.observe(lifecycleOwner, {}) + + val items = viewModel.data.value?.itemViewModels + + val todayHeader = items?.find { it.dayText == "Today" } + assert(todayHeader is ScheduleDayGroupItemViewModel) + assertEquals("Today", todayHeader?.dayText) + + assertEquals(null, todayHeader?.items?.find { it is ScheduleMissingItemsGroupItemViewModel }) + } + + @Test + fun `Courses map correctly`() { + val courses = listOf( + Course(id = 1, name = "Course 1"), + Course(id = 2, name = "Course 2") + ) + + every { courseManager.getCoursesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(courses) + } + + val plannerItems = listOf( + createToDoItem(1, "To Do item"), + createPlannerItem(1, 1, PlannableType.ASSIGNMENT, SubmissionState(submitted = true), Date()), + createPlannerItem(2, 2, PlannableType.ASSIGNMENT, SubmissionState(submitted = true), Date()) + ) + + every { plannerManager.getPlannerItemsAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(plannerItems) + } + + viewModel = createViewModel() + viewModel.getDataForDate(Date().toApiString()) + viewModel.data.observe(lifecycleOwner, {}) + + val items = viewModel.data.value?.itemViewModels + + val todayHeader = items?.find { it.dayText == "Today" } + assert(todayHeader is ScheduleDayGroupItemViewModel) + assertEquals("Today", todayHeader?.dayText) + + assertEquals(3, todayHeader?.items?.count { it is ScheduleCourseItemViewModel }) + + val courseItemViewModels = todayHeader?.items?.filterIsInstance() + val firstCourseItemViewModel = courseItemViewModels?.find { it.data.courseName == "Course 1" } + assertEquals(true, firstCourseItemViewModel?.data?.openable) + assertEquals(1, firstCourseItemViewModel?.data?.plannerItems?.size) + assertEquals("Plannable 1", firstCourseItemViewModel?.data?.plannerItems?.get(0)?.data?.title) + assertEquals(true, firstCourseItemViewModel?.data?.plannerItems?.get(0)?.data?.openable) + + val secondCourseItemViewModel = courseItemViewModels?.find { it.data.courseName == "Course 2" } + assertEquals(true, secondCourseItemViewModel?.data?.openable) + assertEquals(1, secondCourseItemViewModel?.data?.plannerItems?.size) + assertEquals("Plannable 2", secondCourseItemViewModel?.data?.plannerItems?.get(0)?.data?.title) + assertEquals(true, secondCourseItemViewModel?.data?.plannerItems?.get(0)?.data?.openable) + + val todoCourseItemViewModel = courseItemViewModels?.find { it.data.courseName == "To Do" } + assertEquals(false, todoCourseItemViewModel?.data?.openable) + assertEquals(1, todoCourseItemViewModel?.data?.plannerItems?.size) + assertEquals("To Do item", todoCourseItemViewModel?.data?.plannerItems?.get(0)?.data?.title) + assertEquals(false, todoCourseItemViewModel?.data?.plannerItems?.get(0)?.data?.openable) + } + + + @Test + fun `Submitted items are marked as done`() { + val course = Course(id = 1) + + every { courseManager.getCoursesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course)) + } + + val plannerItems = listOf( + createPlannerItem( + courseId = course.id, assignmentId = 1, PlannableType.ASSIGNMENT, SubmissionState(submitted = true), + Date() + ) + ) + + every { plannerManager.getPlannerItemsAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(plannerItems) + } + + every { plannerManager.createPlannerOverrideAsync(any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Fail() + } + + viewModel = createViewModel() + viewModel.getDataForDate(Date().toApiString()) + viewModel.data.observe(lifecycleOwner, {}) + + val items = viewModel.data.value?.itemViewModels + + val todayHeader = items?.find { it.dayText == "Today" } + assert(todayHeader is ScheduleDayGroupItemViewModel) + assertEquals("Today", todayHeader?.dayText) + + val courseItemViewModel = todayHeader?.items?.get(0) as ScheduleCourseItemViewModel + + assertEquals(true, courseItemViewModel.data.openable) + + assertEquals(1, courseItemViewModel.data.plannerItems.size) + val plannerItemViewModel = courseItemViewModel.data.plannerItems[0] + + assertEquals(true, plannerItemViewModel.completed) + } + + @Test + fun `Mark item as done error`() { + val course = Course(id = 1) + + every { courseManager.getCoursesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course)) + } + + val plannerItems = listOf( + createPlannerItem( + courseId = course.id, assignmentId = 1, PlannableType.ASSIGNMENT, SubmissionState(submitted = false), + Date() + ) + ) + + every { plannerManager.getPlannerItemsAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(plannerItems) + } + + every { plannerManager.createPlannerOverrideAsync(any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Fail() + } + + viewModel = createViewModel() + viewModel.getDataForDate(Date().toApiString()) + viewModel.data.observe(lifecycleOwner, {}) + + val items = viewModel.data.value?.itemViewModels + + val todayHeader = items?.find { it.dayText == "Today" } + assert(todayHeader is ScheduleDayGroupItemViewModel) + assertEquals("Today", todayHeader?.dayText) + + val courseItemViewModel = todayHeader?.items?.get(0) as ScheduleCourseItemViewModel + + assertEquals(true, courseItemViewModel.data.openable) + + assertEquals(1, courseItemViewModel.data.plannerItems.size) + val plannerItemViewModel = courseItemViewModel.data.plannerItems[0] + + assertEquals(false, plannerItemViewModel.completed) + plannerItemViewModel.markAsDone.invoke(plannerItemViewModel, true) + assertEquals(false, plannerItemViewModel.completed) + } + + @Test + fun `Update item as not done`() { + val course = Course(id = 1) + + every { courseManager.getCoursesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course)) + } + + val plannerItems = listOf( + createPlannerItem( + courseId = course.id, + assignmentId = 1, + PlannableType.ASSIGNMENT, + SubmissionState(submitted = true), + Date(), + createPlannerOverride(1, PlannableType.ASSIGNMENT, 1, true) + ) + ) + + every { plannerManager.getPlannerItemsAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(plannerItems) + } + + every { plannerManager.updatePlannerOverrideAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(createPlannerOverride(1, PlannableType.ASSIGNMENT, 1, false)) + } + + viewModel = createViewModel() + viewModel.getDataForDate(Date().toApiString()) + viewModel.data.observe(lifecycleOwner, {}) + + val items = viewModel.data.value?.itemViewModels + + val todayHeader = items?.find { it.dayText == "Today" } + assert(todayHeader is ScheduleDayGroupItemViewModel) + assertEquals("Today", todayHeader?.dayText) + + val courseItemViewModel = todayHeader?.items?.get(0) as ScheduleCourseItemViewModel + + assertEquals(true, courseItemViewModel.data.openable) + + assertEquals(1, courseItemViewModel.data.plannerItems.size) + val plannerItemViewModel = courseItemViewModel.data.plannerItems[0] + + assertEquals(true, plannerItemViewModel.completed) + plannerItemViewModel.markAsDone.invoke(plannerItemViewModel, false) + assertEquals(false, plannerItemViewModel.completed) + } + + @Test + fun `Mark item as done`() { + val course = Course(id = 1) + + every { courseManager.getCoursesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course)) + } + + val plannerItems = listOf( + createPlannerItem( + courseId = course.id, + assignmentId = 1, + PlannableType.ASSIGNMENT, + SubmissionState(), + Date() + ) + ) + + every { plannerManager.getPlannerItemsAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(plannerItems) + } + + every { plannerManager.createPlannerOverrideAsync(any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(createPlannerOverride(1, PlannableType.ASSIGNMENT, 1, true)) + } + + viewModel = createViewModel() + viewModel.getDataForDate(Date().toApiString()) + viewModel.data.observe(lifecycleOwner, {}) + + val items = viewModel.data.value?.itemViewModels + + val todayHeader = items?.find { it.dayText == "Today" } + assert(todayHeader is ScheduleDayGroupItemViewModel) + assertEquals("Today", todayHeader?.dayText) + + val courseItemViewModel = todayHeader?.items?.get(0) as ScheduleCourseItemViewModel + + assertEquals(true, courseItemViewModel.data.openable) + + assertEquals(1, courseItemViewModel.data.plannerItems.size) + val plannerItemViewModel = courseItemViewModel.data.plannerItems[0] + + assertEquals(false, plannerItemViewModel.completed) + plannerItemViewModel.markAsDone.invoke(plannerItemViewModel, true) + assertEquals(true, plannerItemViewModel.completed) + } + + @Test + fun `ToDo items map correctly`() { + val plannerItems = listOf( + createToDoItem(1, "To Do item") + ) + + every { plannerManager.getPlannerItemsAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(plannerItems) + } + + viewModel = createViewModel() + viewModel.getDataForDate(Date().toApiString()) + viewModel.data.observe(lifecycleOwner, {}) + + val items = viewModel.data.value?.itemViewModels + + val todayHeader = items?.find { it.dayText == "Today" } + assert(todayHeader is ScheduleDayGroupItemViewModel) + assertEquals("Today", todayHeader?.dayText) + + val courseItem = todayHeader?.items?.get(0) as ScheduleCourseItemViewModel + + assertEquals("To Do", courseItem.data.courseName) + assertEquals(1, courseItem.data.plannerItems.size) + + val plannerItemViewModel = courseItem.data.plannerItems[0] + + assertEquals("To Do item", plannerItemViewModel.data.title) + assertEquals(false, plannerItemViewModel.data.openable) + assertEquals(PlannerItemType.TO_DO, plannerItemViewModel.data.type) + } + + @Test + fun `Assignment maps correctly`() { + val course = Course(id = 1) + + every { courseManager.getCoursesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course)) + } + + val plannerItems = listOf( + createPlannerItem( + courseId = course.id, + assignmentId = 1, + PlannableType.ASSIGNMENT, + SubmissionState(), + Date() + ) + ) + + every { plannerManager.getPlannerItemsAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(plannerItems) + } + + viewModel = createViewModel() + viewModel.getDataForDate(Date().toApiString()) + viewModel.data.observe(lifecycleOwner, {}) + + val items = viewModel.data.value?.itemViewModels + + val todayHeader = items?.find { it.dayText == "Today" } + assert(todayHeader is ScheduleDayGroupItemViewModel) + assertEquals("Today", todayHeader?.dayText) + + val courseItemViewModel = todayHeader?.items?.get(0) as ScheduleCourseItemViewModel + + assertEquals(true, courseItemViewModel.data.openable) + + assertEquals(1, courseItemViewModel.data.plannerItems.size) + val plannerItemViewModel = courseItemViewModel.data.plannerItems[0] + + assertEquals("Plannable 1", plannerItemViewModel.data.title) + assertEquals(true, plannerItemViewModel.data.openable) + assertEquals(PlannerItemType.ASSIGNMENT, plannerItemViewModel.data.type) + assertEquals(null, plannerItemViewModel.data.points) + } + + @Test + fun `Day titles set up correctly`() { + viewModel = createViewModel() + viewModel.getDataForDate(Date().toApiString()) + viewModel.data.observe(lifecycleOwner, {}) + + val items = viewModel.data.value?.itemViewModels + + assertEquals(7, items?.size) + + val todayHeader = items?.find { it.dayText == "Today" } + assert(todayHeader is ScheduleDayGroupItemViewModel) + assertEquals("Today", todayHeader?.dayText) + val todayIndex = items!!.indexOf(todayHeader) + + if (todayIndex != 0) { + val yesterdayHeader = viewModel.data.value?.itemViewModels?.get(todayIndex - 1) + val yesterdayHeaderItemViewModel = yesterdayHeader as ScheduleDayGroupItemViewModel + assertEquals("Yesterday", yesterdayHeaderItemViewModel.dayText) + } + + if (todayIndex != 6) { + val tomorrowHeader = viewModel.data.value?.itemViewModels?.get(todayIndex + 1) + val tomorrowHeaderItemViewModel = tomorrowHeader as ScheduleDayGroupItemViewModel + assertEquals("Tomorrow", tomorrowHeaderItemViewModel.dayText) + } + } + + @Test + fun `Empty view`() { + viewModel = createViewModel() + viewModel.getDataForDate(Date().toApiString()) + viewModel.data.observe(lifecycleOwner, {}) + + val items = viewModel.data.value?.itemViewModels + + assertEquals(7, items?.size) + + items?.forEach { + assertEquals(1, it.items.size) + assert(it.items[0] is ScheduleEmptyItemViewModel) + } + + } + + @Test + fun `Chips are set correctly`() { + val course = Course(id = 1) + + every { courseManager.getCoursesAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(course)) + } + + val plannerItems = listOf( + createPlannerItem(course.id, 1, PlannableType.ASSIGNMENT, SubmissionState(late = true), Date()), + createPlannerItem(course.id, 2, PlannableType.ASSIGNMENT, SubmissionState(graded = true), Date()), + createPlannerItem( + course.id, + 3, + PlannableType.ASSIGNMENT, + SubmissionState(excused = true, graded = true), + Date() + ), + createPlannerItem( + course.id, + 4, + PlannableType.ASSIGNMENT, + SubmissionState(graded = true, late = true), + Date() + ), + createPlannerItem(course.id, 5, PlannableType.ANNOUNCEMENT, SubmissionState(), Date(), newActivity = true), + createPlannerItem( + course.id, + 6, + PlannableType.DISCUSSION_TOPIC, + SubmissionState(late = true, excused = true, withFeedback = true), + Date(), + newActivity = true + ) + ) + + every { plannerManager.getPlannerItemsAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(plannerItems) + } + + val assignments = listOf( + createAssignment(5, 1, discussionTopicHeader = DiscussionTopicHeader(4, unreadCount = 2)), + createAssignment(6, 1, discussionTopicHeader = DiscussionTopicHeader(5, unreadCount = 1)) + ).map { DataResult.Success(it) } + + mockAssignments(assignments) + + viewModel = createViewModel() + viewModel.getDataForDate(Date().toApiString()) + viewModel.data.observe(lifecycleOwner, {}) + + val items = viewModel.data.value?.itemViewModels + val dayGroup = items?.find { it.dayText == "Today" } + assertEquals("Today", dayGroup?.dayText) + + val courseItem = dayGroup?.items?.get(0) + assert(courseItem is ScheduleCourseItemViewModel) + val courseItemViewModel = courseItem as ScheduleCourseItemViewModel + assertEquals(6, courseItemViewModel.data.plannerItems.size) + + val latePlannerItem = courseItem.data.plannerItems[0] + assertEquals(1, latePlannerItem.data.chips.size) + assert(latePlannerItem.data.chips.any { it.data.text == "Late" }) + + val gradedPlannerItem = courseItem.data.plannerItems[1] + assertEquals(1, gradedPlannerItem.data.chips.size) + assert(gradedPlannerItem.data.chips.any { it.data.text == "Graded" }) + + val excusedPlannerItem = courseItem.data.plannerItems[2] + assertEquals(1, excusedPlannerItem.data.chips.size) + assert(excusedPlannerItem.data.chips.any { it.data.text == "Excused" }) + + val gradedLatePlannerItem = courseItem.data.plannerItems[3] + assertEquals(2, gradedLatePlannerItem.data.chips.size) + assert( + gradedLatePlannerItem.data.chips.any { it.data.text == "Graded" } + && gradedLatePlannerItem.data.chips.any { it.data.text == "Late" } + ) + + val unreadPlannerItem = courseItem.data.plannerItems[4] + assertEquals(1, unreadPlannerItem.data.chips.size) + assert(unreadPlannerItem.data.chips.any { it.data.text == "2 Replies" }) + + val unreadGradedLateExcusedPlannerItem = courseItem.data.plannerItems[5] + assertEquals(4, unreadGradedLateExcusedPlannerItem.data.chips.size) + assert( + unreadGradedLateExcusedPlannerItem.data.chips.any { it.data.text == "1 Reply" } + && unreadGradedLateExcusedPlannerItem.data.chips.any { it.data.text == "Late" } + && unreadGradedLateExcusedPlannerItem.data.chips.any { it.data.text == "Excused" } + && unreadGradedLateExcusedPlannerItem.data.chips.any { it.data.text == "Feedback" } + ) + + } + + private fun createPlannerItem( + courseId: Long, + assignmentId: Long, + plannableType: PlannableType, + submissionState: SubmissionState, + date: Date, + plannerOverride: PlannerOverride? = null, + newActivity: Boolean = false, + todoDate: String? = null + ): PlannerItem { + val plannable = Plannable( + id = assignmentId, + title = "Plannable $assignmentId", + courseId, + null, + null, + null, + date, + assignmentId, + todoDate + ) + return PlannerItem( + courseId, + null, + null, + null, + null, + plannableType, + plannable, + date, + null, + submissionState, + plannerOverride = plannerOverride, + newActivity = newActivity + ) + } + + private fun createPlannerOverride( + id: Long, + plannableType: PlannableType, + plannableId: Long, + markedAsComplete: Boolean + ): PlannerOverride { + return PlannerOverride( + id = id, + plannableType = plannableType, + plannableId = plannableId, + markedComplete = markedAsComplete + ) + } + + private fun createAssignment( + id: Long, + courseId: Long, + submission: Submission? = null, + discussionTopicHeader: DiscussionTopicHeader? = null, + name: String? = null + ): Assignment { + return Assignment( + id = id, + submission = submission, + discussionTopicHeader = discussionTopicHeader, + courseId = courseId, + name = name + ) + } + + private fun createSubmission(id: Long, grade: String?, late: Boolean, excused: Boolean): Submission { + return Submission(id = id, grade = grade, late = late, excused = excused) + } + + private fun createToDoItem(id: Long, title: String): PlannerItem { + val plannable = Plannable(id = id, title = title, null, null, null, null, Date(), null, null) + return PlannerItem( + plannable = plannable, + plannableType = PlannableType.PLANNER_NOTE, + plannableDate = Date(), + courseId = null, + contextName = null, + contextType = null, + groupId = null, + htmlUrl = null, + plannerOverride = null, + submissionState = null, + userId = null, + newActivity = false + ) + } + + private fun setupStrings() { + every { resources.getString(R.string.schedule_tag_graded) } returns "Graded" + every { resources.getString(R.plurals.schedule_tag_replies) } returns "Replies" + every { resources.getString(R.string.schedule_tag_feedback) } returns "Feedback" + every { resources.getString(R.string.schedule_tag_late) } returns "Late" + every { resources.getString(R.string.schedule_tag_redo) } returns "Redo" + every { resources.getString(R.string.schedule_tag_excused) } returns "Excused" + every { resources.getString(R.string.tomorrow) } returns "Tomorrow" + every { resources.getString(R.string.yesterday) } returns "Yesterday" + every { resources.getString(R.string.today) } returns "Today" + every { resources.getString(R.string.schedule_todo_title) } returns "To Do" + every { resources.getQuantityString(R.plurals.schedule_tag_replies, 2, 2) } returns "2 Replies" + every { resources.getQuantityString(R.plurals.schedule_tag_replies, 1, 1) } returns "1 Reply" + } + + private fun createViewModel(): ScheduleViewModel { + return ScheduleViewModel( + apiPrefs, + resources, + plannerManager, + courseManager, + userManager, + calendarEventManager, + assignmentManager, + missingItemsPrefs, + dateTimeProvider + ) + } + + private fun mockAssignments(assignments: List> = emptyList()) { + val assignmentDeferred: Deferred> = mockk() + every { assignmentManager.getAssignmentAsync(any(), any(), any()) } returns assignmentDeferred + val listOfAssignmentDeferred = assignments.map { assignmentDeferred } + coEvery { listOfAssignmentDeferred.awaitAll() } returns assignments + } + + private fun mockCalendarEvents(calendarEvents: List> = emptyList()) { + val calendarEventDeferred: Deferred> = mockk() + every { calendarEventManager.getCalendarEventAsync(any(), any()) } returns calendarEventDeferred + val listOfCalendarEventDeferred = calendarEvents.map { calendarEventDeferred } + coEvery { listOfCalendarEventDeferred.awaitAll() } returns calendarEvents + } +} \ No newline at end of file diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/utils/DateExtensionsTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/utils/DateExtensionsTest.kt new file mode 100644 index 0000000000..b7ef26144b --- /dev/null +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/utils/DateExtensionsTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.utils + +import junit.framework.Assert.assertEquals +import org.junit.Test +import java.util.* + +class DateExtensionsTest { + + @Test + fun `Get last Sunday`() { + val calendar = Calendar.getInstance() + calendar.set(2021, 6, 1) + val currentDate = calendar.time + val lastSunday = currentDate.getLastSunday() + + calendar.set(2021, 5, 27) + val expectedDate = calendar.time + + assertEquals(expectedDate, lastSunday) + } + + @Test + fun `Get next Saturday`() { + val calendar = Calendar.getInstance() + calendar.set(2021, 5, 29) + val currentDate = calendar.time + val nextSaturday = currentDate.getNextSaturday() + + calendar.set(2021, 6, 3) + val expectedDate = calendar.time + + assertEquals(expectedDate, nextSaturday) + } + + @Test + fun `Is Yesterday`() { + val calendar = Calendar.getInstance() + calendar.set(2021, 6, 1) + val currentDate = calendar.time + calendar.set(2021, 5, 30) + val yesterday = calendar.time + assert(yesterday.isPreviousDay(currentDate)) + } + + @Test + fun `Is Tomorrow`() { + val calendar = Calendar.getInstance() + calendar.set(2021, 6, 31) + val currentDate = calendar.time + calendar.set(2021, 7, 1) + val tomorrow = calendar.time + assert(tomorrow.isNextDay(currentDate)) + } +} \ No newline at end of file