diff --git a/apps/buildSrc/src/main/java/GlobalDependencies.kt b/apps/buildSrc/src/main/java/GlobalDependencies.kt index 7120f7076b..a10251eb0d 100644 --- a/apps/buildSrc/src/main/java/GlobalDependencies.kt +++ b/apps/buildSrc/src/main/java/GlobalDependencies.kt @@ -45,6 +45,7 @@ object Versions { const val ENCRYPTED_SHARED_PREFERENCES = "1.0.0" const val JAVA_JWT = "4.5.0" const val GLANCE = "1.1.1" + const val LIVEDATA = "1.9.0" } object Libs { @@ -131,7 +132,6 @@ object Libs { const val LIFECYCLE_COMPILER = "androidx.lifecycle:lifecycle-compiler:${Versions.LIFECYCLE}" const val COMPOSE_VIEW_MODEL = "androidx.lifecycle:lifecycle-viewmodel-compose:${Versions.LIFECYCLE}" const val COMPOSE_NAVIGATION = "androidx.navigation:navigation-compose:2.8.9" - /* Media and content handling */ const val PSPDFKIT = "com.pspdfkit:pspdfkit:${Versions.PSPDFKIT}" const val MEDIA3 = "androidx.media3:media3-exoplayer:${Versions.MEDIA3}" diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt index c065eb0492..9a62c37a2e 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt @@ -141,7 +141,7 @@ class AssignmentReminderE2ETest: ParentComposeTest() { Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") assignmentReminderPage.clickAddReminder() - val reminderDateOneDay = futureDueDate.apply { add(Calendar.DAY_OF_MONTH, -1) } + val reminderDateOneDay = futureDueDate.apply { add(Calendar.DAY_OF_MONTH, -1) }.apply { add(Calendar.HOUR, -1) } Log.d(STEP_TAG, "Select '1 Day Before'.") assignmentReminderPage.clickCustomReminderOption() assignmentReminderPage.selectDate(reminderDateOneDay) diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt index e879991018..12c9a02bf9 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt @@ -40,6 +40,7 @@ import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.AlertType import com.instructure.canvasapi2.models.AlertWorkflowState import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.utils.toApiString import com.instructure.pandautils.utils.toFormattedString @@ -104,7 +105,7 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { fun testDisplayDueDate() { val data = setupData() val calendar = Calendar.getInstance().apply { set(2023, 0, 31, 23, 59, 0) } - val expectedDueDate = "January 31, 2023 11:59 PM" + val expectedDueDate = "Jan 31, 2023 11:59 PM" val course = data.courses.values.first() val assignmentWithNoDueDate = data.addAssignment(course.id, name = "Test Assignment", dueAt = calendar.time.toApiString()) @@ -113,6 +114,41 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { assignmentDetailsPage.assertDisplaysDate(expectedDueDate) } + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) + fun testDisplayDueDates() { + val data = setupData() + var calendar = Calendar.getInstance().apply { set(2023, 0, 29, 23, 59, 0) } + val expectedReplyToTopicDueDate = "Jan 29, 2023 11:59 PM" + val replyToTopicDueDate = calendar.time.toApiString() + + calendar = Calendar.getInstance().apply { set(2023, 0, 31, 23, 59, 0) } + val expectedReplyToEntryDueDate = "Jan 31, 2023 11:59 PM" + val replyToEntryDueDate = calendar.time.toApiString() + val course = data.courses.values.first() + + val checkpoints = listOf( + Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + dueAt = replyToTopicDueDate, + pointsPossible = 10.0 + ), + Checkpoint( + name = "Reply to Entry", + tag = "reply_to_entry", + dueAt = replyToEntryDueDate, + pointsPossible = 10.0 + ) + ) + val assignmentWithNoDueDate = data.addAssignment(course.id, name = "Test Assignment", dueAt = calendar.time.toApiString(), checkpoints = checkpoints) + + gotoAssignment(data, assignmentWithNoDueDate) + + assignmentDetailsPage.assertDisplaysDate(expectedReplyToTopicDueDate, 0) + assignmentDetailsPage.assertDisplaysDate(expectedReplyToEntryDueDate, 1) + } + @Test fun testNavigating_viewAssignmentDetails() { // Test clicking on the Assignment item in the Assignment List to load the Assignment Details Page @@ -272,6 +308,38 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { assignmentReminderPage.assertReminderSectionDisplayed() } + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) + fun testReminderSectionsAreVisibleWhenThereAreNoFutureDueDates() { + val data = setupData() + val course = data.courses.values.first() + + val pastDate = Calendar.getInstance().apply { + add(Calendar.DAY_OF_MONTH, -1) + }.time.toApiString() + + val checkpoints = listOf( + Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + dueAt = pastDate, + pointsPossible = 10.0 + ), + Checkpoint( + name = "Reply to Entry", + tag = "reply_to_entry", + dueAt = pastDate, + pointsPossible = 10.0 + ) + ) + val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = pastDate, checkpoints = checkpoints) + + gotoAssignment(data, assignment) + + assignmentDetailsPage.assertReminderViewDisplayed(0) + assignmentDetailsPage.assertReminderViewDisplayed(1) + } + @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) fun testReminderSectionIsVisibleWhenThereIsNoDueDate() { diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCalendarInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCalendarInteractionTest.kt index 1e00fb8342..ec849a83f3 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCalendarInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCalendarInteractionTest.kt @@ -51,7 +51,7 @@ class ParentCalendarInteractionTest : CalendarInteractionTest() { override val activityRule = ParentActivityTestRule(LoginActivity::class.java) private val dashboardPage = DashboardPage() - private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) + private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(), composeTestRule) override fun goToCalendar(data: MockCanvas) { val parent = data.parents.first() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt index e5340473d0..b46c703105 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt @@ -18,6 +18,7 @@ package com.instructure.parentapp.utils import androidx.compose.ui.test.junit4.createAndroidComposeRule +import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage import com.instructure.canvas.espresso.common.pages.AssignmentReminderPage import com.instructure.canvas.espresso.common.pages.compose.CalendarEventCreateEditPage import com.instructure.canvas.espresso.common.pages.compose.CalendarEventDetailsPage @@ -31,6 +32,7 @@ import com.instructure.canvas.espresso.common.pages.compose.InboxDetailsPage import com.instructure.canvas.espresso.common.pages.compose.InboxSignatureSettingsPage import com.instructure.canvas.espresso.common.pages.compose.RecipientPickerPage import com.instructure.canvas.espresso.common.pages.compose.SettingsPage +import com.instructure.espresso.ModuleItemInteractions import com.instructure.parentapp.features.login.LoginActivity import com.instructure.parentapp.ui.pages.compose.AddStudentBottomPage import com.instructure.parentapp.ui.pages.compose.AlertsPage @@ -80,6 +82,7 @@ abstract class ParentComposeTest : ParentTest() { protected val calendarFilterPage = CalendarFilterPage(composeTestRule) protected val assignmentReminderPage = AssignmentReminderPage(composeTestRule) protected val inboxSignatureSettingsPage = InboxSignatureSettingsPage(composeTestRule) + protected val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(), composeTestRule) override fun displaysPageObjects() = Unit } diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt index 69fadbaef3..d8a7a3e533 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt @@ -19,14 +19,12 @@ package com.instructure.parentapp.utils import com.instructure.canvas.espresso.CanvasTest import com.instructure.canvas.espresso.common.pages.AboutPage -import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage import com.instructure.canvas.espresso.common.pages.CanvasNetworkSignInPage import com.instructure.canvas.espresso.common.pages.InboxPage import com.instructure.canvas.espresso.common.pages.LegalPage import com.instructure.canvas.espresso.common.pages.LoginFindSchoolPage import com.instructure.canvas.espresso.common.pages.LoginLandingPage import com.instructure.canvas.espresso.common.pages.LoginSignInPage -import com.instructure.espresso.ModuleItemInteractions import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity import com.instructure.parentapp.ui.pages.classic.DashboardPage @@ -46,7 +44,6 @@ abstract class ParentTest : CanvasTest() { val dashboardPage = DashboardPage() val leftSideNavigationDrawerPage = LeftSideNavigationDrawerPage() val helpPage = HelpPage() - val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) val syllabusPage = SyllabusPage() val frontPagePage = FrontPagePage() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt index b0e5045ed2..03fd6afebe 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt @@ -18,7 +18,6 @@ package com.instructure.student.ui.e2e.classic import android.os.Environment import android.util.Log -import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.test.espresso.Espresso import androidx.test.espresso.intent.Intents import androidx.test.platform.app.InstrumentationRegistry @@ -44,28 +43,22 @@ import com.instructure.dataseeding.util.Randomizer import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin import com.instructure.student.ui.utils.extensions.uploadTextFile import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Rule import org.junit.Test import java.io.File import java.io.FileWriter @HiltAndroidTest -class FilesE2ETest: StudentTest() { +class FilesE2ETest: StudentComposeTest() { override fun displaysPageObjects() = Unit override fun enableAndConfigureAccessibilityChecks() = Unit - @get:Rule - val composeTestRule = createEmptyComposeRule() - - val assignmentListPage by lazy { AssignmentListPage(composeTestRule) } - @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.FILES, TestCategory.E2E) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt index aec21f7875..2669a7eb0e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt @@ -21,14 +21,14 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.getDateInCanvasCalendarFormat import com.instructure.student.R -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class GradesE2ETest: StudentTest() { +class GradesE2ETest: StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ModulesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ModulesE2ETest.kt index 43e1ce6830..d7e6ea4b2f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ModulesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ModulesE2ETest.kt @@ -35,14 +35,14 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class ModulesE2ETest: StudentTest() { +class ModulesE2ETest: StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt index 0f78466c0b..cb94725be3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt @@ -19,13 +19,12 @@ package com.instructure.student.ui.e2e.classic import android.content.Intent import android.net.Uri import android.util.Log -import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.test.espresso.Espresso import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector import com.instructure.canvas.espresso.annotations.E2E -import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType @@ -33,25 +32,20 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Rule import org.junit.Test @HiltAndroidTest -class ShareExtensionE2ETest: StudentTest() { +class ShareExtensionE2ETest: StudentComposeTest() { override fun displaysPageObjects() = Unit override fun enableAndConfigureAccessibilityChecks() = Unit - @get:Rule - val composeTestRule = createEmptyComposeRule() - - val assignmentListPage by lazy { AssignmentListPage(composeTestRule) } - + @Stub @E2E @Test fun shareExtensionE2ETest() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ImportantDatesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ImportantDatesE2ETest.kt index e4b3e95757..685cfb37fd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ImportantDatesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ImportantDatesE2ETest.kt @@ -31,7 +31,7 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedDataForK5 import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest @@ -41,7 +41,7 @@ import java.util.Date import java.util.Locale @HiltAndroidTest -class ImportantDatesE2ETest : StudentTest() { +class ImportantDatesE2ETest : StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ScheduleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ScheduleE2ETest.kt index 18d84e3242..54f76a8ea3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ScheduleE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ScheduleE2ETest.kt @@ -33,7 +33,7 @@ import com.instructure.espresso.page.getStringFromResource import com.instructure.espresso.page.withAncestor import com.instructure.student.R import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedDataForK5 import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest @@ -46,7 +46,7 @@ import java.util.Locale import java.util.TimeZone @HiltAndroidTest -class ScheduleE2ETest : StudentTest() { +class ScheduleE2ETest : StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineGradesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineGradesE2ETest.kt index a0aae2496b..8e6e10ff9f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineGradesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineGradesE2ETest.kt @@ -37,7 +37,7 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.getDateInCanvasCalendarFormat -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin import com.instructure.student.ui.utils.offline.OfflineTestUtils @@ -46,7 +46,7 @@ import org.junit.After import org.junit.Test @HiltAndroidTest -class OfflineGradesE2ETest : StudentTest() { +class OfflineGradesE2ETest : StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt index 49cd03c2a7..00754ec014 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt @@ -36,7 +36,7 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin import com.instructure.student.ui.utils.offline.OfflineTestUtils @@ -47,7 +47,7 @@ import org.junit.After import org.junit.Test @HiltAndroidTest -class OfflineModulesE2ETest : StudentTest() { +class OfflineModulesE2ETest : StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt index 44c8737c32..1575acf651 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt @@ -34,6 +34,7 @@ import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.utils.toApiString import com.instructure.dataseeding.model.SubmissionType @@ -141,7 +142,7 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { val data = setUpData() goToAssignmentList() val calendar = Calendar.getInstance().apply { set(2023, 0, 31, 23, 59, 0) } - val expectedDueDate = "January 31, 2023 11:59 PM" + val expectedDueDate = "Jan 31, 2023 11:59 PM" val course = data.courses.values.first() val assignmentWithNoDueDate = data.addAssignment(course.id, name = "Test Assignment", dueAt = calendar.time.toApiString()) assignmentListPage.refreshAssignmentList() @@ -150,6 +151,43 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { assignmentDetailsPage.assertDisplaysDate(expectedDueDate) } + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) + fun testDisplayDueDates() { + val data = setUpData() + goToAssignmentList() + + var calendar = Calendar.getInstance().apply { set(2023, 0, 29, 23, 59, 0) } + val expectedReplyToTopicDueDate = "Jan 29, 2023 11:59 PM" + val replyToTopicDueDate = calendar.time.toApiString() + + calendar = Calendar.getInstance().apply { set(2023, 0, 31, 23, 59, 0) } + val expectedReplyToEntryDueDate = "Jan 31, 2023 11:59 PM" + val replyToEntryDueDate = calendar.time.toApiString() + val course = data.courses.values.first() + + val checkpoints = listOf( + Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + dueAt = replyToTopicDueDate, + pointsPossible = 10.0 + ), + Checkpoint( + name = "Reply to Entry", + tag = "reply_to_entry", + dueAt = replyToEntryDueDate, + pointsPossible = 10.0 + ) + ) + val assignmentWithNoDueDate = data.addAssignment(course.id, name = "Test Assignment", dueAt = calendar.time.toApiString(), checkpoints = checkpoints) + assignmentListPage.refreshAssignmentList() + assignmentListPage.clickAssignment(assignmentWithNoDueDate) + + assignmentDetailsPage.assertDisplaysDate(expectedReplyToTopicDueDate, 0) + assignmentDetailsPage.assertDisplaysDate(expectedReplyToEntryDueDate, 1) + } + @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) fun testNavigating_viewAssignmentDetails() { @@ -403,6 +441,38 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { assignmentReminderPage.assertReminderSectionDisplayed() } + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) + fun testReminderSectionsAreVisibleWhenThereAreNoFutureDueDates() { + val data = setUpData() + val course = data.courses.values.first() + + val pastDate = Calendar.getInstance().apply { + add(Calendar.DAY_OF_MONTH, -1) + }.time.toApiString() + + val checkpoints = listOf( + Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + dueAt = pastDate, + pointsPossible = 10.0 + ), + Checkpoint( + name = "Reply to Entry", + tag = "reply_to_entry", + dueAt = pastDate, + pointsPossible = 10.0 + ) + ) + val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = pastDate, checkpoints = checkpoints) + goToAssignmentList() + + assignmentListPage.clickAssignment(assignment) + assignmentDetailsPage.assertReminderViewDisplayed(0) + assignmentDetailsPage.assertReminderViewDisplayed(1) + } + @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) fun testReminderSectionIsVisibleWhenThereIsNoDueDate() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt index fd27493201..00b7bece2d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt @@ -57,7 +57,7 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.R import com.instructure.student.ui.pages.classic.WebViewTextCheck -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest @@ -68,7 +68,7 @@ import java.net.URLEncoder @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class ModuleInteractionTest : StudentTest() { +class ModuleInteractionTest : StudentComposeTest() { @BindValue @JvmField diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt index 6ec94e8480..f6517ba42c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt @@ -32,7 +32,7 @@ import com.instructure.canvasapi2.models.CourseSettings import com.instructure.dataseeding.util.ago import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest @@ -42,7 +42,7 @@ import java.util.UUID @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class NotificationInteractionTest : StudentTest() { +class NotificationInteractionTest : StudentComposeTest() { @BindValue @JvmField diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt index 419e0b450a..86ac820578 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt @@ -22,7 +22,6 @@ import android.content.Intent import android.net.Uri import android.provider.MediaStore import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.test.espresso.Espresso import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intending @@ -40,7 +39,6 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.Stub -import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvas.espresso.mockcanvas.addAssignment import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager @@ -49,7 +47,7 @@ import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment import com.instructure.pandautils.utils.FilePrefs -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest @@ -64,7 +62,7 @@ import java.io.File @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class PickerSubmissionUploadInteractionTest : StudentTest() { +class PickerSubmissionUploadInteractionTest : StudentComposeTest() { @BindValue @JvmField @@ -76,11 +74,6 @@ class PickerSubmissionUploadInteractionTest : StudentTest() { private lateinit var activity : Activity private lateinit var activityResult: Instrumentation.ActivityResult - @get:Rule - val composeTestRule = createEmptyComposeRule() - - val assignmentListPage by lazy { AssignmentListPage(composeTestRule) } - @Before fun setUp() { // Read this at set-up, because it may become null soon thereafter 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 index 2f8d1f77f4..bcddb0dca6 100644 --- 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 @@ -35,7 +35,7 @@ import com.instructure.espresso.page.getStringFromResource import com.instructure.pandautils.utils.date.DateTimeProvider import com.instructure.student.R import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.di.FakeDateTimeProvider import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.BindValue @@ -48,7 +48,7 @@ import javax.inject.Inject @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class ScheduleInteractionTest : StudentTest() { +class ScheduleInteractionTest : StudentComposeTest() { @BindValue @JvmField diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt index c95950398d..bbbea2b0ff 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt @@ -48,7 +48,7 @@ class StudentCalendarInteractionTest : CalendarInteractionTest() { override val activityRule = StudentActivityTestRule(LoginActivity::class.java) private val dashboardPage = DashboardPage() - private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) + private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(), composeTestRule) private val discussionDetailsPage = DiscussionDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item)) override fun goToCalendar(data: MockCanvas) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt index c20f50172a..e06b5fa056 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt @@ -17,7 +17,6 @@ package com.instructure.student.ui.interaction import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.web.webdriver.Locator import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils @@ -27,7 +26,6 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.Stub -import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvas.espresso.mockcanvas.addAssignment import com.instructure.canvas.espresso.mockcanvas.addFileToCourse @@ -46,19 +44,18 @@ import com.instructure.canvasapi2.models.RubricCriterionRating import com.instructure.canvasapi2.models.SubmissionComment import com.instructure.espresso.handleWorkManagerTask import com.instructure.student.ui.pages.classic.WebViewTextCheck -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules import org.hamcrest.Matchers -import org.junit.Rule import org.junit.Test import java.util.Date @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class SubmissionDetailsInteractionTest : StudentTest() { +class SubmissionDetailsInteractionTest : StudentComposeTest() { @BindValue @JvmField @@ -68,11 +65,6 @@ class SubmissionDetailsInteractionTest : StudentTest() { private lateinit var course: Course - @get:Rule - val composeTestRule = createEmptyComposeRule() - - val assignmentListPage by lazy { AssignmentListPage(composeTestRule) } - // Should be able to add a comment on a submission @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt index 2e6b05b79d..914f24e310 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt @@ -39,7 +39,7 @@ import com.instructure.canvasapi2.models.Quiz import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest @@ -49,7 +49,7 @@ import org.junit.Test @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class TodoInteractionTest : StudentTest() { +class TodoInteractionTest : StudentComposeTest() { @BindValue @JvmField diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt index 8622e5c1f5..a0019b2354 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt @@ -16,6 +16,7 @@ package com.instructure.student.ui.pages.classic import androidx.appcompat.widget.AppCompatButton +import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import com.instructure.canvas.espresso.CanvasTest @@ -36,7 +37,7 @@ import com.instructure.espresso.typeText import com.instructure.student.R import org.hamcrest.Matchers.allOf -class StudentAssignmentDetailsPage(moduleItemInteractions: ModuleItemInteractions): AssignmentDetailsPage(moduleItemInteractions) { +class StudentAssignmentDetailsPage(moduleItemInteractions: ModuleItemInteractions, composeTestRule: ComposeTestRule): AssignmentDetailsPage(moduleItemInteractions, composeTestRule) { fun addBookmark(bookmarkName: String) { openOverflowMenu() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt index 3c3b86c9a6..5ccf520462 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt @@ -35,7 +35,10 @@ import com.instructure.canvas.espresso.common.pages.compose.SelectContextPage import com.instructure.canvas.espresso.common.pages.compose.SettingsPage import com.instructure.canvas.espresso.common.pages.compose.SmartSearchPage import com.instructure.canvas.espresso.common.pages.compose.SmartSearchPreferencesPage +import com.instructure.espresso.ModuleItemInteractions +import com.instructure.student.R import com.instructure.student.activity.LoginActivity +import com.instructure.student.ui.pages.classic.StudentAssignmentDetailsPage import org.junit.Rule abstract class StudentComposeTest : StudentTest() { @@ -60,4 +63,11 @@ abstract class StudentComposeTest : StudentTest() { val smartSearchPreferencesPage = SmartSearchPreferencesPage(composeTestRule) val assignmentListPage = AssignmentListPage(composeTestRule) val inboxSignatureSettingsPage = InboxSignatureSettingsPage(composeTestRule) + val assignmentDetailsPage = StudentAssignmentDetailsPage( + ModuleItemInteractions( + R.id.moduleName, + R.id.next_item, + R.id.prev_item + ), composeTestRule + ) } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt index ecad0fc8e0..5e43e6a33c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt @@ -85,7 +85,6 @@ import com.instructure.student.ui.pages.classic.QuizTakingPage import com.instructure.student.ui.pages.classic.RemoteConfigSettingsPage import com.instructure.student.ui.pages.classic.ShareExtensionStatusPage import com.instructure.student.ui.pages.classic.ShareExtensionTargetPage -import com.instructure.student.ui.pages.classic.StudentAssignmentDetailsPage import com.instructure.student.ui.pages.classic.SubmissionDetailsPage import com.instructure.student.ui.pages.classic.SyllabusPage import com.instructure.student.ui.pages.classic.TextSubmissionUploadPage @@ -120,7 +119,6 @@ abstract class StudentTest : CanvasTest() { */ val annotationCommentListPage = AnnotationCommentListPage() val announcementListPage = AnnouncementListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn)) - val assignmentDetailsPage = StudentAssignmentDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item)) val bookmarkPage = BookmarkPage() val canvasWebViewPage = CanvasWebViewPage() val courseBrowserPage = CourseBrowserPage() diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/GradesInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/GradesInteractionTest.kt index 0412d2214e..9580c803ff 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/GradesInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/GradesInteractionTest.kt @@ -34,7 +34,7 @@ import org.junit.Test abstract class GradesInteractionTest : CanvasComposeTest() { private val gradesPage = GradesPage(composeTestRule) - private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) + private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(), composeTestRule) @Test fun groupHeaderCollapsesAndExpandsOnClick() { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SmartSearchInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SmartSearchInteractionTest.kt index e9e68a81c4..76cc1a1879 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SmartSearchInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SmartSearchInteractionTest.kt @@ -35,7 +35,7 @@ abstract class SmartSearchInteractionTest : CanvasComposeTest() { private val smartSearchPage = SmartSearchPage(composeTestRule) private val smartSearchPreferencesPage = SmartSearchPreferencesPage(composeTestRule) - private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) + private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(), composeTestRule) @Test fun assertQuery() { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt index 427092227c..454f337717 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt @@ -18,6 +18,10 @@ package com.instructure.canvas.espresso.common.pages import android.view.View import android.widget.ScrollView +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag import androidx.test.espresso.AmbiguousViewMatcherException import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onData @@ -68,10 +72,9 @@ import org.hamcrest.Matchers.anyOf import org.hamcrest.Matchers.anything import org.hamcrest.Matchers.not -open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteractions) : BasePage(R.id.assignmentDetailsPage) { +open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteractions, private val composeTestRule: ComposeTestRule) : BasePage(R.id.assignmentDetailsPage) { val toolbar by OnViewWithId(R.id.toolbar) val points by OnViewWithId(R.id.points) - val date by OnViewWithId(R.id.dueDateTextView) val submissionTypes by OnViewWithId(R.id.submissionTypesTextView) fun assertDisplayToolbarTitle() { @@ -86,8 +89,8 @@ open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteracti onView(allOf(withText(courseNameText), withParent(R.id.toolbar))).assertDisplayed() } - fun assertDisplaysDate(dateText: String) { - date.assertHasText(dateText) + fun assertDisplaysDate(dateText: String, position: Int = 0) { + composeTestRule.onNodeWithTag("dueDateText-$position").assertTextEquals(dateText).isDisplayed() } fun assertAssignmentDetails(assignment: Assignment) { @@ -254,8 +257,8 @@ open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteracti onView(anyOf(withText(submissionType) + withAncestor(R.id.customPanel), withId(R.id.submissionTypesTextView) + withText(submissionType))).assertDisplayed() } - fun assertReminderViewDisplayed() { - onView(withId(R.id.reminderComposeView)).assertDisplayed() + fun assertReminderViewDisplayed(position: Int = 0) { + composeTestRule.onNodeWithTag("reminderView-$position").assertExists() } fun assertNoDescriptionViewDisplayed() { 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 93eec07573..58ddd511fc 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 @@ -40,6 +40,7 @@ import com.instructure.canvasapi2.models.CanvasColor import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.CanvasTheme +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings @@ -1106,29 +1107,40 @@ fun MockCanvas.addAssignment( withDescription: Boolean = false, gradingType: String = "percent", discussionTopicHeader: DiscussionTopicHeader? = null, - htmlUrl: String? = "" + htmlUrl: String? = "", + submission: Submission? = null, + checkpoints: List = emptyList() ) : Assignment { val assignmentId = newItemId() val submissionTypeListRawStrings = submissionTypeList.map { it.apiString } var assignment = Assignment( - id = assignmentId, - assignmentGroupId = assignmentGroupId, - courseId = courseId, - name = name, - submissionTypesRaw = submissionTypeListRawStrings, - lockInfo = lockInfo, - lockedForUser = lockInfo != null, - userSubmitted = userSubmitted, - dueAt = dueAt, - pointsPossible = pointsPossible.toDouble(), - description = description, - lockAt = lockAt, - unlockAt = unlockAt, - published = true, - allDates = listOf(AssignmentDueDate(id = newItemId(), dueAt = dueAt, lockAt = lockAt, unlockAt = unlockAt)), - gradingType = gradingType, - discussionTopicHeader = discussionTopicHeader, - htmlUrl = htmlUrl + id = assignmentId, + assignmentGroupId = assignmentGroupId, + courseId = courseId, + name = name, + submissionTypesRaw = submissionTypeListRawStrings, + lockInfo = lockInfo, + lockedForUser = lockInfo != null, + userSubmitted = userSubmitted, + dueAt = dueAt, + pointsPossible = pointsPossible.toDouble(), + description = description, + lockAt = lockAt, + unlockAt = unlockAt, + published = true, + allDates = listOf( + AssignmentDueDate( + id = newItemId(), + dueAt = dueAt, + lockAt = lockAt, + unlockAt = unlockAt + ) + ), + gradingType = gradingType, + discussionTopicHeader = discussionTopicHeader, + htmlUrl = htmlUrl, + submission = submission, + checkpoints = checkpoints ) if (isQuizzesNext) { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AssignmentEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AssignmentEndpoints.kt index 1aa91e039c..6b9a62e654 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AssignmentEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AssignmentEndpoints.kt @@ -218,5 +218,6 @@ private fun Assignment.toObserveeAssignment() = ObserveeAssignment( moderatedGrading = moderatedGrading, anonymousGrading = anonymousGrading, allowedAttempts = allowedAttempts, - isStudioEnabled = isStudioEnabled + isStudioEnabled = isStudioEnabled, + checkpoints = checkpoints, ) \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AssignmentAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AssignmentAPI.kt index 12237e9380..87a95648c9 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AssignmentAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AssignmentAPI.kt @@ -67,7 +67,7 @@ object AssignmentAPI { @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=score_statistics&include[]=submission_history") fun getAssignmentWithHistory(@Path("courseId") courseId: Long, @Path("assignmentId") assignmentId: Long): Call - @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=score_statistics&include[]=submission_history") + @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=score_statistics&include[]=submission_history&include[]=checkpoints&include[]=discussion_topic&include[]=sub_assignment_submissions") suspend fun getAssignmentWithHistory( @Path("courseId") courseId: Long, @Path("assignmentId") assignmentId: Long, @@ -77,7 +77,7 @@ object AssignmentAPI { @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=observed_users&include[]=score_statistics&include[]=submission_history") fun getAssignmentIncludeObservees(@Path("courseId") courseId: Long, @Path("assignmentId") assignmentId: Long): Call - @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=observed_users&include[]=score_statistics&include[]=submission_history") + @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=observed_users&include[]=score_statistics&include[]=submission_history&include[]=checkpoints&include[]=discussion_topic&include[]=sub_assignment_submissions") suspend fun getAssignmentIncludeObservees( @Path("courseId") courseId: Long, @Path("assignmentId") assignmentId: Long, diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index 68e112c8fb..8ccbe57bc1 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -2151,6 +2151,8 @@ Write days late Reply to topic Additional replies (%d) + Reply to topic due + Additional replies (%d) due Discussion Checkpoints Multiple Due Dates Course concluded. Unable to send messages! diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/13.json b/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/13.json new file mode 100644 index 0000000000..37a4c85508 --- /dev/null +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/13.json @@ -0,0 +1,722 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "e8a50c8d4caed97be61826c69921684e", + "entities": [ + { + "tableName": "AttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contentType` TEXT, `filename` TEXT, `displayName` TEXT, `url` TEXT, `thumbnailUrl` TEXT, `previewUrl` TEXT, `createdAt` INTEGER, `size` INTEGER NOT NULL, `workerId` TEXT, `submissionCommentId` INTEGER, `submissionId` INTEGER, `attempt` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionCommentId", + "columnName": "submissionCommentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AuthorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, `pronouns` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "EnvironmentFeatureFlags", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `featureFlags` TEXT NOT NULL, PRIMARY KEY(`userId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "featureFlags", + "columnName": "featureFlags", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FileUploadInputEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`workerId` TEXT NOT NULL, `courseId` INTEGER, `assignmentId` INTEGER, `quizId` INTEGER, `quizQuestionId` INTEGER, `position` INTEGER, `parentFolderId` INTEGER, `action` TEXT NOT NULL, `userId` INTEGER, `attachments` TEXT NOT NULL, `submissionId` INTEGER, `filePaths` TEXT NOT NULL, `attemptId` INTEGER, `notificationId` INTEGER, PRIMARY KEY(`workerId`))", + "fields": [ + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quizId", + "columnName": "quizId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quizQuestionId", + "columnName": "quizQuestionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "parentFolderId", + "columnName": "parentFolderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "action", + "columnName": "action", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filePaths", + "columnName": "filePaths", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "workerId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MediaCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mediaId` TEXT NOT NULL, `displayName` TEXT, `url` TEXT, `mediaType` TEXT, `contentType` TEXT, PRIMARY KEY(`mediaId`))", + "fields": [ + { + "fieldPath": "mediaId", + "columnName": "mediaId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaType", + "columnName": "mediaType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "mediaId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SubmissionCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `authorId` INTEGER NOT NULL, `authorName` TEXT, `authorPronouns` TEXT, `comment` TEXT, `createdAt` INTEGER, `mediaCommentId` TEXT, `attemptId` INTEGER, `submissionId` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorName", + "columnName": "authorName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorPronouns", + "columnName": "authorPronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "PendingSubmissionCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `pageId` TEXT NOT NULL, `comment` TEXT, `date` INTEGER NOT NULL, `status` TEXT NOT NULL, `workerId` TEXT, `filePath` TEXT, `attemptId` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filePath", + "columnName": "filePath", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DashboardFileUploadEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`workerId` TEXT NOT NULL, `userId` INTEGER NOT NULL, `title` TEXT, `subtitle` TEXT, `courseId` INTEGER, `assignmentId` INTEGER, `attemptId` INTEGER, `folderId` INTEGER, PRIMARY KEY(`workerId`))", + "fields": [ + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subtitle", + "columnName": "subtitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "workerId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReminderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `htmlUrl` TEXT NOT NULL, `name` TEXT NOT NULL, `text` TEXT NOT NULL, `time` INTEGER NOT NULL, `tag` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ModuleBulkProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`progressId` INTEGER NOT NULL, `allModules` INTEGER NOT NULL, `skipContentTags` INTEGER NOT NULL, `action` TEXT NOT NULL, `courseId` INTEGER NOT NULL, `affectedIds` TEXT NOT NULL, PRIMARY KEY(`progressId`))", + "fields": [ + { + "fieldPath": "progressId", + "columnName": "progressId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allModules", + "columnName": "allModules", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "skipContentTags", + "columnName": "skipContentTags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "action", + "columnName": "action", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "affectedIds", + "columnName": "affectedIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "progressId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "assignment_filter", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userDomain` TEXT NOT NULL, `userId` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `selectedAssignmentFilters` TEXT NOT NULL, `selectedAssignmentStatusFilter` TEXT, `selectedGroupByOption` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userDomain", + "columnName": "userDomain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selectedAssignmentFilters", + "columnName": "selectedAssignmentFilters", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "selectedAssignmentStatusFilter", + "columnName": "selectedAssignmentStatusFilter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "selectedGroupByOption", + "columnName": "selectedGroupByOption", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FileDownloadProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`workerId` TEXT NOT NULL, `fileName` TEXT NOT NULL, `progress` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `filePath` TEXT NOT NULL, PRIMARY KEY(`workerId`))", + "fields": [ + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "filePath", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "workerId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e8a50c8d4caed97be61826c69921684e')" + ] + } +} \ No newline at end of file diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/6.json b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/6.json new file mode 100644 index 0000000000..8bfb3ebb8f --- /dev/null +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/6.json @@ -0,0 +1,5832 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "e0e8981a53e92176b25c0fb1066137d6", + "entities": [ + { + "tableName": "AssignmentDueDateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `assignmentOverrideId` INTEGER, `dueAt` TEXT, `title` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `isBase` INTEGER NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isBase", + "columnName": "isBase", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `submissionTypesRaw` TEXT NOT NULL, `dueAt` TEXT, `pointsPossible` REAL NOT NULL, `courseId` INTEGER NOT NULL, `isGradeGroupsIndividually` INTEGER NOT NULL, `gradingType` TEXT, `needsGradingCount` INTEGER NOT NULL, `htmlUrl` TEXT, `url` TEXT, `quizId` INTEGER NOT NULL, `isUseRubricForGrading` INTEGER NOT NULL, `rubricSettingsId` INTEGER, `allowedExtensions` TEXT NOT NULL, `submissionId` INTEGER, `assignmentGroupId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `isPeerReviews` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, `lockExplanation` TEXT, `discussionTopicHeaderId` INTEGER, `freeFormCriterionComments` INTEGER NOT NULL, `published` INTEGER NOT NULL, `groupCategoryId` INTEGER NOT NULL, `userSubmitted` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `onlyVisibleToOverrides` INTEGER NOT NULL, `anonymousPeerReviews` INTEGER NOT NULL, `moderatedGrading` INTEGER NOT NULL, `anonymousGrading` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `plannerOverrideId` INTEGER, `isStudioEnabled` INTEGER NOT NULL, `inClosedGradingPeriod` INTEGER NOT NULL, `annotatableAttachmentId` INTEGER NOT NULL, `anonymousSubmissions` INTEGER NOT NULL, `omitFromFinalGrade` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentGroupId`) REFERENCES `AssignmentGroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionTypesRaw", + "columnName": "submissionTypesRaw", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeGroupsIndividually", + "columnName": "isGradeGroupsIndividually", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingType", + "columnName": "gradingType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quizId", + "columnName": "quizId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUseRubricForGrading", + "columnName": "isUseRubricForGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricSettingsId", + "columnName": "rubricSettingsId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "allowedExtensions", + "columnName": "allowedExtensions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPeerReviews", + "columnName": "isPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userSubmitted", + "columnName": "userSubmitted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousPeerReviews", + "columnName": "anonymousPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moderatedGrading", + "columnName": "moderatedGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousGrading", + "columnName": "anonymousGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannerOverrideId", + "columnName": "plannerOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isStudioEnabled", + "columnName": "isStudioEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inClosedGradingPeriod", + "columnName": "inClosedGradingPeriod", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "annotatableAttachmentId", + "columnName": "annotatableAttachmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousSubmissions", + "columnName": "anonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "omitFromFinalGrade", + "columnName": "omitFromFinalGrade", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentGroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentGroupId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentGroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `position` INTEGER NOT NULL, `groupWeight` REAL NOT NULL, `rules` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupWeight", + "columnName": "groupWeight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rules", + "columnName": "rules", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `title` TEXT, `dueAt` INTEGER, `isAllDay` INTEGER NOT NULL, `allDayDate` TEXT, `unlockAt` INTEGER, `lockAt` INTEGER, `courseSectionId` INTEGER NOT NULL, `groupId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayDate", + "columnName": "allDayDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentRubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `rubricId` TEXT NOT NULL, PRIMARY KEY(`assignmentId`, `rubricId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricId", + "columnName": "rubricId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId", + "rubricId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentScoreStatisticsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `mean` REAL NOT NULL, `min` REAL NOT NULL, `max` REAL NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mean", + "columnName": "mean", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "min", + "columnName": "min", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "max", + "columnName": "max", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentSetEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `scoringRangeId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `position` INTEGER NOT NULL, `masteryPathId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`masteryPathId`) REFERENCES `MasteryPathEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringRangeId", + "columnName": "scoringRangeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "masteryPathId", + "columnName": "masteryPathId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "MasteryPathEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "masteryPathId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `originalName` TEXT, `courseCode` TEXT, `startAt` TEXT, `endAt` TEXT, `syllabusBody` TEXT, `hideFinalGrades` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `license` TEXT NOT NULL, `termId` INTEGER, `needsGradingCount` INTEGER NOT NULL, `isApplyAssignmentGroupWeights` INTEGER NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, `isFavorite` INTEGER NOT NULL, `accessRestrictedByDate` INTEGER NOT NULL, `imageUrl` TEXT, `bannerImageUrl` TEXT, `isWeightedGradingPeriods` INTEGER NOT NULL, `hasGradingPeriods` INTEGER NOT NULL, `homePage` TEXT, `restrictEnrollmentsToCourseDate` INTEGER NOT NULL, `workflowState` TEXT, `homeroomCourse` INTEGER NOT NULL, `courseColor` TEXT, `gradingScheme` TEXT, `pointsBasedGradingScheme` INTEGER NOT NULL, `scalingFactor` REAL NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`termId`) REFERENCES `TermEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "syllabusBody", + "columnName": "syllabusBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideFinalGrades", + "columnName": "hideFinalGrades", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "license", + "columnName": "license", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "termId", + "columnName": "termId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isApplyAssignmentGroupWeights", + "columnName": "isApplyAssignmentGroupWeights", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessRestrictedByDate", + "columnName": "accessRestrictedByDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imageUrl", + "columnName": "imageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bannerImageUrl", + "columnName": "bannerImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isWeightedGradingPeriods", + "columnName": "isWeightedGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasGradingPeriods", + "columnName": "hasGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homePage", + "columnName": "homePage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "restrictEnrollmentsToCourseDate", + "columnName": "restrictEnrollmentsToCourseDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "homeroomCourse", + "columnName": "homeroomCourse", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseColor", + "columnName": "courseColor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "gradingScheme", + "columnName": "gradingScheme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsBasedGradingScheme", + "columnName": "pointsBasedGradingScheme", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scalingFactor", + "columnName": "scalingFactor", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "TermEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "termId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFilesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`courseId`, `url`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "url" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "CourseGradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `gradingPeriodId` INTEGER NOT NULL, PRIMARY KEY(`courseId`, `gradingPeriodId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`gradingPeriodId`) REFERENCES `GradingPeriodEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "gradingPeriodId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "GradingPeriodEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "gradingPeriodId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseSummary` INTEGER, `restrictQuantitativeData` INTEGER NOT NULL, PRIMARY KEY(`courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseSummary", + "columnName": "courseSummary", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "restrictQuantitativeData", + "columnName": "restrictQuantitativeData", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `fullContentSync` INTEGER NOT NULL, `tabs` TEXT NOT NULL, `fullFileSync` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullContentSync", + "columnName": "fullContentSync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullFileSync", + "columnName": "fullFileSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DashboardCardEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isK5Subject` INTEGER NOT NULL, `shortName` TEXT, `originalName` TEXT, `courseCode` TEXT, `position` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isK5Subject", + "columnName": "isK5Subject", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionEntryAttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionEntryId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionEntryId`, `remoteFileId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionEntryId", + "remoteFileId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `updatedAt` TEXT, `createdAt` TEXT, `authorId` INTEGER, `description` TEXT, `userId` INTEGER NOT NULL, `parentId` INTEGER NOT NULL, `message` TEXT, `deleted` INTEGER NOT NULL, `totalChildren` INTEGER NOT NULL, `unreadChildren` INTEGER NOT NULL, `ratingCount` INTEGER NOT NULL, `ratingSum` INTEGER NOT NULL, `editorId` INTEGER NOT NULL, `_hasRated` INTEGER NOT NULL, `replyIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChildren", + "columnName": "totalChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadChildren", + "columnName": "unreadChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingCount", + "columnName": "ratingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingSum", + "columnName": "ratingSum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editorId", + "columnName": "editorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "_hasRated", + "columnName": "_hasRated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyIds", + "columnName": "replyIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionParticipantEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `pronouns` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionTopicHeaderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `discussionType` TEXT, `title` TEXT, `message` TEXT, `htmlUrl` TEXT, `postedDate` INTEGER, `delayedPostDate` INTEGER, `lastReplyDate` INTEGER, `requireInitialPost` INTEGER NOT NULL, `discussionSubentryCount` INTEGER NOT NULL, `readState` TEXT, `unreadCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `assignmentId` INTEGER, `locked` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `pinned` INTEGER NOT NULL, `authorId` INTEGER, `podcastUrl` TEXT, `groupCategoryId` TEXT, `announcement` INTEGER NOT NULL, `permissionId` INTEGER, `published` INTEGER NOT NULL, `allowRating` INTEGER NOT NULL, `onlyGradersCanRate` INTEGER NOT NULL, `sortByRating` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `lockAt` INTEGER, `userCanSeePosts` INTEGER NOT NULL, `specificSections` TEXT, `anonymousState` TEXT, `replyRequiredCount` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`authorId`) REFERENCES `DiscussionParticipantEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`permissionId`) REFERENCES `DiscussionTopicPermissionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionType", + "columnName": "discussionType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postedDate", + "columnName": "postedDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "delayedPostDate", + "columnName": "delayedPostDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastReplyDate", + "columnName": "lastReplyDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "requireInitialPost", + "columnName": "requireInitialPost", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionSubentryCount", + "columnName": "discussionSubentryCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readState", + "columnName": "readState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unreadCount", + "columnName": "unreadCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "podcastUrl", + "columnName": "podcastUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "announcement", + "columnName": "announcement", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "permissionId", + "columnName": "permissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowRating", + "columnName": "allowRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyGradersCanRate", + "columnName": "onlyGradersCanRate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortByRating", + "columnName": "sortByRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userCanSeePosts", + "columnName": "userCanSeePosts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "specificSections", + "columnName": "specificSections", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "anonymousState", + "columnName": "anonymousState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replyRequiredCount", + "columnName": "replyRequiredCount", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionParticipantEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "authorId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "DiscussionTopicPermissionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "permissionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicPermissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `discussionTopicHeaderId` INTEGER NOT NULL, `attach` INTEGER NOT NULL, `update` INTEGER NOT NULL, `delete` INTEGER NOT NULL, `reply` INTEGER NOT NULL, FOREIGN KEY(`discussionTopicHeaderId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attach", + "columnName": "attach", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delete", + "columnName": "delete", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reply", + "columnName": "reply", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicHeaderId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicRemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionId`, `remoteFileId`), FOREIGN KEY(`discussionId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionId", + "columnName": "discussionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionId", + "remoteFileId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicSectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionTopicId` INTEGER NOT NULL, `sectionId` INTEGER NOT NULL, PRIMARY KEY(`discussionTopicId`, `sectionId`), FOREIGN KEY(`discussionTopicId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionTopicId", + "columnName": "discussionTopicId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionTopicId", + "sectionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "EnrollmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `role` TEXT NOT NULL, `type` TEXT NOT NULL, `courseId` INTEGER, `courseSectionId` INTEGER, `enrollmentState` TEXT, `userId` INTEGER NOT NULL, `computedCurrentScore` REAL, `computedFinalScore` REAL, `computedCurrentGrade` TEXT, `computedFinalGrade` TEXT, `multipleGradingPeriodsEnabled` INTEGER NOT NULL, `totalsForAllGradingPeriodsOption` INTEGER NOT NULL, `currentPeriodComputedCurrentScore` REAL, `currentPeriodComputedFinalScore` REAL, `currentPeriodComputedCurrentGrade` TEXT, `currentPeriodComputedFinalGrade` TEXT, `currentGradingPeriodId` INTEGER NOT NULL, `currentGradingPeriodTitle` TEXT, `associatedUserId` INTEGER NOT NULL, `lastActivityAt` INTEGER, `limitPrivilegesToCourseSection` INTEGER NOT NULL, `observedUserId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`observedUserId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseSectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "computedCurrentScore", + "columnName": "computedCurrentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "computedFinalScore", + "columnName": "computedFinalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "computedCurrentGrade", + "columnName": "computedCurrentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "computedFinalGrade", + "columnName": "computedFinalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "multipleGradingPeriodsEnabled", + "columnName": "multipleGradingPeriodsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalsForAllGradingPeriodsOption", + "columnName": "totalsForAllGradingPeriodsOption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPeriodComputedCurrentScore", + "columnName": "currentPeriodComputedCurrentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedFinalScore", + "columnName": "currentPeriodComputedFinalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedCurrentGrade", + "columnName": "currentPeriodComputedCurrentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedFinalGrade", + "columnName": "currentPeriodComputedFinalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentGradingPeriodId", + "columnName": "currentGradingPeriodId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentGradingPeriodTitle", + "columnName": "currentGradingPeriodTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "associatedUserId", + "columnName": "associatedUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivityAt", + "columnName": "lastActivityAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "limitPrivilegesToCourseSection", + "columnName": "limitPrivilegesToCourseSection", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "observedUserId", + "columnName": "observedUserId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "observedUserId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "courseSectionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileFolderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `createdDate` INTEGER, `updatedDate` INTEGER, `unlockDate` INTEGER, `lockDate` INTEGER, `isLocked` INTEGER NOT NULL, `isHidden` INTEGER NOT NULL, `isLockedForUser` INTEGER NOT NULL, `isHiddenForUser` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `size` INTEGER NOT NULL, `contentType` TEXT, `url` TEXT, `displayName` TEXT, `thumbnailUrl` TEXT, `parentFolderId` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `filesCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `foldersCount` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `foldersUrl` TEXT, `filesUrl` TEXT, `fullName` TEXT, `forSubmissions` INTEGER NOT NULL, `canUpload` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedDate", + "columnName": "updatedDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unlockDate", + "columnName": "unlockDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockDate", + "columnName": "lockDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLockedForUser", + "columnName": "isLockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHiddenForUser", + "columnName": "isHiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentFolderId", + "columnName": "parentFolderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filesCount", + "columnName": "filesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "foldersCount", + "columnName": "foldersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "foldersUrl", + "columnName": "foldersUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesUrl", + "columnName": "filesUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "forSubmissions", + "columnName": "forSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canUpload", + "columnName": "canUpload", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "EditDashboardItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `name` TEXT NOT NULL, `isFavorite` INTEGER NOT NULL, `enrollmentState` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ExternalToolAttributesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `url` TEXT, `newTab` INTEGER NOT NULL, `resourceLinkid` TEXT, `contentId` INTEGER, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "newTab", + "columnName": "newTab", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceLinkid", + "columnName": "resourceLinkid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`enrollmentId` INTEGER NOT NULL, `htmlUrl` TEXT NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, PRIMARY KEY(`enrollmentId`), FOREIGN KEY(`enrollmentId`) REFERENCES `EnrollmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "enrollmentId", + "columnName": "enrollmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "enrollmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "EnrollmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "enrollmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `startDate` TEXT, `endDate` TEXT, `weight` REAL NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startDate", + "columnName": "startDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endDate", + "columnName": "endDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `avatarUrl` TEXT, `isPublic` INTEGER NOT NULL, `membersCount` INTEGER NOT NULL, `joinLevel` TEXT, `courseId` INTEGER NOT NULL, `accountId` INTEGER NOT NULL, `role` TEXT, `groupCategoryId` INTEGER NOT NULL, `storageQuotaMb` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `concluded` INTEGER NOT NULL, `canAccess` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "membersCount", + "columnName": "membersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinLevel", + "columnName": "joinLevel", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "storageQuotaMb", + "columnName": "storageQuotaMb", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concluded", + "columnName": "concluded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canAccess", + "columnName": "canAccess", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GroupUserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LocalFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `createdDate` INTEGER NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MasteryPathAssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `overrideId` INTEGER NOT NULL, `assignmentSetId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentSetId`) REFERENCES `AssignmentSetEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "overrideId", + "columnName": "overrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentSetId", + "columnName": "assignmentSetId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentSetEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentSetId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MasteryPathEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isLocked` INTEGER NOT NULL, `selectedSetId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selectedSetId", + "columnName": "selectedSetId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleContentDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `pointsPossible` TEXT, `dueAt` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `moduleId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `title` TEXT, `indent` INTEGER NOT NULL, `type` TEXT, `htmlUrl` TEXT, `url` TEXT, `published` INTEGER, `contentId` INTEGER NOT NULL, `externalUrl` TEXT, `pageUrl` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`moduleId`) REFERENCES `ModuleObjectEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "indent", + "columnName": "indent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageUrl", + "columnName": "pageUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleObjectEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleObjectEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `name` TEXT, `unlockAt` TEXT, `sequentialProgress` INTEGER NOT NULL, `prerequisiteIds` TEXT, `state` TEXT, `completedAt` TEXT, `published` INTEGER, `itemCount` INTEGER NOT NULL, `itemsUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sequentialProgress", + "columnName": "sequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prerequisiteIds", + "columnName": "prerequisiteIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "completedAt", + "columnName": "completedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemCount", + "columnName": "itemCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemsUrl", + "columnName": "itemsUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "NeedsGradingCountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sectionId` INTEGER NOT NULL, `needsGradingCount` INTEGER NOT NULL, PRIMARY KEY(`sectionId`), FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sectionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `createdAt` INTEGER, `updatedAt` INTEGER, `hideFromStudents` INTEGER NOT NULL, `status` TEXT, `body` TEXT, `frontPage` INTEGER NOT NULL, `published` INTEGER NOT NULL, `editingRoles` TEXT, `htmlUrl` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hideFromStudents", + "columnName": "hideFromStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "frontPage", + "columnName": "frontPage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editingRoles", + "columnName": "editingRoles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PlannerOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `plannableType` TEXT NOT NULL, `plannableId` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, `markedComplete` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannableType", + "columnName": "plannableType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "plannableId", + "columnName": "plannableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dismissed", + "columnName": "dismissed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "markedComplete", + "columnName": "markedComplete", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `displayName` TEXT, `fileName` TEXT, `contentType` TEXT, `url` TEXT, `size` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `unlockAt` TEXT, `locked` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `lockAt` TEXT, `hiddenForUser` INTEGER NOT NULL, `thumbnailUrl` TEXT, `modifiedAt` TEXT, `lockedForUser` INTEGER NOT NULL, `previewUrl` TEXT, `lockExplanation` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hiddenForUser", + "columnName": "hiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "modifiedAt", + "columnName": "modifiedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RubricCriterionAssessmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `assignmentId` INTEGER NOT NULL, `ratingId` TEXT, `points` REAL, `comments` TEXT, PRIMARY KEY(`id`, `assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingId", + "columnName": "ratingId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "comments", + "columnName": "comments", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `criterionUseRange` INTEGER NOT NULL, `ignoreForScoring` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "criterionUseRange", + "columnName": "criterionUseRange", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ignoreForScoring", + "columnName": "ignoreForScoring", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionRatingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `rubricCriterionId` TEXT NOT NULL, PRIMARY KEY(`id`, `rubricCriterionId`), FOREIGN KEY(`rubricCriterionId`) REFERENCES `RubricCriterionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rubricCriterionId", + "columnName": "rubricCriterionId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "rubricCriterionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "RubricCriterionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "rubricCriterionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `pointsPossible` REAL NOT NULL, `title` TEXT NOT NULL, `isReusable` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `isReadOnly` INTEGER NOT NULL, `freeFormCriterionComments` INTEGER NOT NULL, `hideScoreTotal` INTEGER NOT NULL, `hidePoints` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isReusable", + "columnName": "isReusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isReadOnly", + "columnName": "isReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hideScoreTotal", + "columnName": "hideScoreTotal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidePoints", + "columnName": "hidePoints", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemAssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentOverrideId` INTEGER NOT NULL, `scheduleItemId` TEXT NOT NULL, PRIMARY KEY(`assignmentOverrideId`, `scheduleItemId`), FOREIGN KEY(`assignmentOverrideId`) REFERENCES `AssignmentOverrideEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`scheduleItemId`) REFERENCES `ScheduleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduleItemId", + "columnName": "scheduleItemId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentOverrideId", + "scheduleItemId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentOverrideEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentOverrideId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "ScheduleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "scheduleItemId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `description` TEXT, `startAt` TEXT, `endAt` TEXT, `isAllDay` INTEGER NOT NULL, `allDayAt` TEXT, `locationAddress` TEXT, `locationName` TEXT, `htmlUrl` TEXT, `contextCode` TEXT, `effectiveContextCode` TEXT, `isHidden` INTEGER NOT NULL, `importantDates` INTEGER NOT NULL, `assignmentId` INTEGER, `type` TEXT NOT NULL, `itemType` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayAt", + "columnName": "allDayAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationAddress", + "columnName": "locationAddress", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationName", + "columnName": "locationName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextCode", + "columnName": "contextCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "effectiveContextCode", + "columnName": "effectiveContextCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "importantDates", + "columnName": "importantDates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itemType", + "columnName": "itemType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER, `startAt` TEXT, `endAt` TEXT, `totalStudents` INTEGER NOT NULL, `restrictEnrollmentsToSectionDates` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "totalStudents", + "columnName": "totalStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "restrictEnrollmentsToSectionDates", + "columnName": "restrictEnrollmentsToSectionDates", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionDiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`submissionId` INTEGER NOT NULL, `discussionEntryId` INTEGER NOT NULL, PRIMARY KEY(`submissionId`, `discussionEntryId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "submissionId", + "discussionEntryId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `attempt` INTEGER NOT NULL, `submittedAt` INTEGER, `commentCreated` INTEGER, `mediaContentType` TEXT, `mediaCommentUrl` TEXT, `mediaCommentDisplay` TEXT, `body` TEXT, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, `workflowState` TEXT, `submissionType` TEXT, `previewUrl` TEXT, `url` TEXT, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `mediaCommentId` TEXT, `assignmentId` INTEGER NOT NULL, `userId` INTEGER, `graderId` INTEGER, `groupId` INTEGER, `pointsDeducted` REAL, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `postedAt` INTEGER, `gradingPeriodId` INTEGER, `customGradeStatusId` INTEGER, `hasSubAssignmentSubmissions` INTEGER NOT NULL, PRIMARY KEY(`id`, `attempt`), FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submittedAt", + "columnName": "submittedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "commentCreated", + "columnName": "commentCreated", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaContentType", + "columnName": "mediaContentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaCommentUrl", + "columnName": "mediaCommentUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaCommentDisplay", + "columnName": "mediaCommentDisplay", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionType", + "columnName": "submissionType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "graderId", + "columnName": "graderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pointsDeducted", + "columnName": "pointsDeducted", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postedAt", + "columnName": "postedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "customGradeStatusId", + "columnName": "customGradeStatusId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasSubAssignmentSubmissions", + "columnName": "hasSubAssignmentSubmissions", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "attempt" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `autoSyncEnabled` INTEGER NOT NULL, `syncFrequency` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoSyncEnabled", + "columnName": "autoSyncEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncFrequency", + "columnName": "syncFrequency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifiOnly", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TabEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `label` TEXT, `type` TEXT NOT NULL, `htmlUrl` TEXT, `externalUrl` TEXT, `visibility` TEXT NOT NULL, `isHidden` INTEGER NOT NULL, `position` INTEGER NOT NULL, `ltiUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ltiUrl", + "columnName": "ltiUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "TermEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `startAt` TEXT, `endAt` TEXT, `isGroupTerm` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isGroupTerm", + "columnName": "isGroupTerm", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserCalendarEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ics` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ics", + "columnName": "ics", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `shortName` TEXT, `loginId` TEXT, `avatarUrl` TEXT, `primaryEmail` TEXT, `email` TEXT, `sortableName` TEXT, `bio` TEXT, `enrollmentIndex` INTEGER NOT NULL, `lastLogin` TEXT, `locale` TEXT, `effective_locale` TEXT, `pronouns` TEXT, `k5User` INTEGER NOT NULL, `rootAccount` TEXT, `isFakeStudent` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginId", + "columnName": "loginId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "primaryEmail", + "columnName": "primaryEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortableName", + "columnName": "sortableName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bio", + "columnName": "bio", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enrollmentIndex", + "columnName": "enrollmentIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastLogin", + "columnName": "lastLogin", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "effective_locale", + "columnName": "effective_locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "k5User", + "columnName": "k5User", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rootAccount", + "columnName": "rootAccount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFakeStudent", + "columnName": "isFakeStudent", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "QuizEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `mobileUrl` TEXT, `htmlUrl` TEXT, `description` TEXT, `quizType` TEXT, `assignmentGroupId` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `questionCount` INTEGER NOT NULL, `pointsPossible` TEXT, `isLockQuestionsAfterAnswering` INTEGER NOT NULL, `dueAt` TEXT, `timeLimit` INTEGER NOT NULL, `shuffleAnswers` INTEGER NOT NULL, `showCorrectAnswers` INTEGER NOT NULL, `scoringPolicy` TEXT, `accessCode` TEXT, `ipFilter` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `hideResults` TEXT, `showCorrectAnswersAt` TEXT, `hideCorrectAnswersAt` TEXT, `unlockAt` TEXT, `oneTimeResults` INTEGER NOT NULL, `lockAt` TEXT, `questionTypes` TEXT NOT NULL, `hasAccessCode` INTEGER NOT NULL, `oneQuestionAtATime` INTEGER NOT NULL, `requireLockdownBrowser` INTEGER NOT NULL, `requireLockdownBrowserForResults` INTEGER NOT NULL, `allowAnonymousSubmissions` INTEGER NOT NULL, `published` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `isOnlyVisibleToOverrides` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mobileUrl", + "columnName": "mobileUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quizType", + "columnName": "quizType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questionCount", + "columnName": "questionCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isLockQuestionsAfterAnswering", + "columnName": "isLockQuestionsAfterAnswering", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timeLimit", + "columnName": "timeLimit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shuffleAnswers", + "columnName": "shuffleAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showCorrectAnswers", + "columnName": "showCorrectAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringPolicy", + "columnName": "scoringPolicy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accessCode", + "columnName": "accessCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ipFilter", + "columnName": "ipFilter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideResults", + "columnName": "hideResults", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "showCorrectAnswersAt", + "columnName": "showCorrectAnswersAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideCorrectAnswersAt", + "columnName": "hideCorrectAnswersAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oneTimeResults", + "columnName": "oneTimeResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "questionTypes", + "columnName": "questionTypes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasAccessCode", + "columnName": "hasAccessCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneQuestionAtATime", + "columnName": "oneQuestionAtATime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowser", + "columnName": "requireLockdownBrowser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowserForResults", + "columnName": "requireLockdownBrowserForResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowAnonymousSubmissions", + "columnName": "allowAnonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isOnlyVisibleToOverrides", + "columnName": "isOnlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockInfoEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `modulePrerequisiteNames` TEXT, `unlockAt` TEXT, `lockedModuleId` INTEGER, `assignmentId` INTEGER, `moduleId` INTEGER, `pageId` INTEGER, FOREIGN KEY(`moduleId`) REFERENCES `ModuleContentDetailsEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`pageId`) REFERENCES `PageEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "modulePrerequisiteNames", + "columnName": "modulePrerequisiteNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleContentDetailsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "PageEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pageId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockedModuleEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `unlockAt` TEXT, `isRequireSequentialProgress` INTEGER NOT NULL, `lockInfoId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`lockInfoId`) REFERENCES `LockInfoEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isRequireSequentialProgress", + "columnName": "isRequireSequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockInfoId", + "columnName": "lockInfoId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "LockInfoEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockInfoId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleNameEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `lockedModuleId` INTEGER NOT NULL, FOREIGN KEY(`lockedModuleId`) REFERENCES `LockedModuleEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "LockedModuleEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockedModuleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleCompletionRequirementEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT, `minScore` REAL NOT NULL, `maxScore` REAL NOT NULL, `completed` INTEGER, `moduleId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "minScore", + "columnName": "minScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxScore", + "columnName": "maxScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `fileName` TEXT, `courseId` INTEGER NOT NULL, `url` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "ConferenceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `conferenceKey` TEXT, `conferenceType` TEXT, `description` TEXT, `duration` INTEGER NOT NULL, `endedAt` INTEGER, `hasAdvancedSettings` INTEGER NOT NULL, `joinUrl` TEXT, `longRunning` INTEGER NOT NULL, `startedAt` INTEGER, `title` TEXT, `url` TEXT, `contextType` TEXT NOT NULL, `contextId` INTEGER NOT NULL, `record` INTEGER, `users` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conferenceKey", + "columnName": "conferenceKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "conferenceType", + "columnName": "conferenceType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endedAt", + "columnName": "endedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAdvancedSettings", + "columnName": "hasAdvancedSettings", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinUrl", + "columnName": "joinUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longRunning", + "columnName": "longRunning", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startedAt", + "columnName": "startedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "record", + "columnName": "record", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "users", + "columnName": "users", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ConferenceRecordingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`recordingId` TEXT NOT NULL, `conferenceId` INTEGER NOT NULL, `createdAtMillis` INTEGER NOT NULL, `durationMinutes` INTEGER NOT NULL, `playbackUrl` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`recordingId`), FOREIGN KEY(`conferenceId`) REFERENCES `ConferenceEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "recordingId", + "columnName": "recordingId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conferenceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAtMillis", + "columnName": "createdAtMillis", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durationMinutes", + "columnName": "durationMinutes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playbackUrl", + "columnName": "playbackUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "recordingId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ConferenceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "conferenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFeaturesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `features` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "features", + "columnName": "features", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contentType` TEXT, `filename` TEXT, `displayName` TEXT, `url` TEXT, `thumbnailUrl` TEXT, `previewUrl` TEXT, `createdAt` INTEGER, `size` INTEGER NOT NULL, `workerId` TEXT, `submissionCommentId` INTEGER, `submissionId` INTEGER, `attempt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionCommentId`) REFERENCES `SubmissionCommentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionCommentId", + "columnName": "submissionCommentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionCommentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionCommentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MediaCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mediaId` TEXT NOT NULL, `submissionId` INTEGER NOT NULL, `attemptId` INTEGER NOT NULL, `displayName` TEXT, `url` TEXT, `mediaType` TEXT, `contentType` TEXT, PRIMARY KEY(`mediaId`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "mediaId", + "columnName": "mediaId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaType", + "columnName": "mediaType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "mediaId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "AuthorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, `pronouns` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SubmissionCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `authorId` INTEGER NOT NULL, `authorName` TEXT, `authorPronouns` TEXT, `comment` TEXT, `createdAt` INTEGER, `mediaCommentId` TEXT, `attemptId` INTEGER, `submissionId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorName", + "columnName": "authorName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorPronouns", + "columnName": "authorPronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "DiscussionTopicEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unreadEntries` TEXT NOT NULL, `participantIds` TEXT NOT NULL, `viewIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadEntries", + "columnName": "unreadEntries", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantIds", + "columnName": "participantIds", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viewIds", + "columnName": "viewIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CourseSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `tabs` TEXT NOT NULL, `additionalFilesStarted` INTEGER NOT NULL, `progressState` TEXT NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "additionalFilesStarted", + "columnName": "additionalFilesStarted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FileSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `fileName` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `additionalFile` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseSyncProgressEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "fileId", + "columnName": "fileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "additionalFile", + "columnName": "additionalFile", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncProgressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "StudioMediaProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ltiLaunchId` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "ltiLaunchId", + "columnName": "ltiLaunchId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CustomGradeStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CheckpointEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `assignmentId` INTEGER NOT NULL, `name` TEXT, `tag` TEXT, `pointsPossible` REAL, `dueAt` TEXT, `onlyVisibleToOverrides` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubAssignmentSubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `submissionId` INTEGER NOT NULL, `submissionAttempt` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `latePolicyStatus` TEXT, `customGradeStatusId` INTEGER, `subAssignmentTag` TEXT, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `userId` INTEGER NOT NULL, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, FOREIGN KEY(`submissionId`, `submissionAttempt`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionAttempt", + "columnName": "submissionAttempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latePolicyStatus", + "columnName": "latePolicyStatus", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customGradeStatusId", + "columnName": "customGradeStatusId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subAssignmentTag", + "columnName": "subAssignmentTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "submissionAttempt" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e0e8981a53e92176b25c0fb1066137d6')" + ] + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CheckpointDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CheckpointDaoTest.kt new file mode 100644 index 0000000000..c5518f2d68 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CheckpointDaoTest.kt @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.Checkpoint +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.AssignmentEntity +import com.instructure.pandautils.room.offline.entities.AssignmentGroupEntity +import com.instructure.pandautils.room.offline.entities.CheckpointEntity +import com.instructure.pandautils.room.offline.entities.CourseEntity +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class CheckpointDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var checkpointDao: CheckpointDao + private lateinit var assignmentDao: AssignmentDao + private lateinit var assignmentGroupDao: AssignmentGroupDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + checkpointDao = db.checkpointDao() + assignmentDao = db.assignmentDao() + assignmentGroupDao = db.assignmentGroupDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertAndFind() = runTest { + setupAssignment(1L, 1L) + + val checkpoint = CheckpointEntity( + assignmentId = 1L, + name = "Checkpoint 1", + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null + ) + + checkpointDao.insert(checkpoint) + + val result = checkpointDao.findByAssignmentId(1L) + assertEquals(1, result.size) + assertEquals("Checkpoint 1", result[0].name) + assertEquals("reply_to_topic", result[0].tag) + assertEquals(10.0, result[0].pointsPossible) + } + + @Test + fun testInsertMultipleCheckpoints() = runTest { + setupAssignment(1L, 1L) + + val checkpoint1 = CheckpointEntity( + assignmentId = 1L, + name = "Reply to Topic", + tag = "reply_to_topic", + pointsPossible = 5.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null + ) + + val checkpoint2 = CheckpointEntity( + assignmentId = 1L, + name = "Required Replies", + tag = "reply_to_entry", + pointsPossible = 5.0, + dueAt = "2025-10-20T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null + ) + + checkpointDao.insertAll(listOf(checkpoint1, checkpoint2)) + + val result = checkpointDao.findByAssignmentId(1L) + assertEquals(2, result.size) + assertTrue(result.any { it.tag == "reply_to_topic" }) + assertTrue(result.any { it.tag == "reply_to_entry" }) + } + + @Test + fun testDeleteByAssignmentId() = runTest { + setupAssignment(1L, 1L) + + val checkpoint = CheckpointEntity( + assignmentId = 1L, + name = "Checkpoint 1", + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null + ) + + checkpointDao.insert(checkpoint) + checkpointDao.deleteByAssignmentId(1L) + + val result = checkpointDao.findByAssignmentId(1L) + assertTrue(result.isEmpty()) + } + + @Test + fun testCascadeDelete() = runTest { + setupAssignment(1L, 1L) + + val checkpoint = CheckpointEntity( + assignmentId = 1L, + name = "Checkpoint 1", + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null + ) + + checkpointDao.insert(checkpoint) + + val assignmentEntity = assignmentDao.findById(1L)!! + assignmentDao.delete(assignmentEntity) + + val result = checkpointDao.findByAssignmentId(1L) + assertTrue(result.isEmpty()) + } + + @Test + fun testToApiModel() { + val checkpointEntity = CheckpointEntity( + assignmentId = 1L, + name = "Reply to Topic", + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = true, + lockAt = "2025-10-22T23:59:59Z", + unlockAt = "2025-10-10T00:00:00Z" + ) + + val checkpoint = checkpointEntity.toApiModel() + + assertEquals("Reply to Topic", checkpoint.name) + assertEquals("reply_to_topic", checkpoint.tag) + assertEquals(10.0, checkpoint.pointsPossible) + assertEquals("2025-10-15T23:59:59Z", checkpoint.dueAt) + assertEquals(true, checkpoint.onlyVisibleToOverrides) + assertEquals("2025-10-22T23:59:59Z", checkpoint.lockAt) + assertEquals("2025-10-10T00:00:00Z", checkpoint.unlockAt) + } + + @Test + fun testConstructorFromApiModel() { + val checkpoint = Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + overrides = null, + onlyVisibleToOverrides = true, + lockAt = "2025-10-22T23:59:59Z", + unlockAt = "2025-10-10T00:00:00Z" + ) + + val entity = CheckpointEntity(checkpoint, 1L) + + assertEquals(1L, entity.assignmentId) + assertEquals("Reply to Topic", entity.name) + assertEquals("reply_to_topic", entity.tag) + assertEquals(10.0, entity.pointsPossible) + assertEquals("2025-10-15T23:59:59Z", entity.dueAt) + assertEquals(true, entity.onlyVisibleToOverrides) + assertEquals("2025-10-22T23:59:59Z", entity.lockAt) + assertEquals("2025-10-10T00:00:00Z", entity.unlockAt) + } + + private suspend fun setupAssignment(assignmentId: Long, courseId: Long) { + val courseEntity = CourseEntity(Course(id = courseId)) + courseDao.insert(courseEntity) + + val assignmentGroupEntity = AssignmentGroupEntity(AssignmentGroup(id = 1L), courseId) + assignmentGroupDao.insert(assignmentGroupEntity) + + val assignmentEntity = AssignmentEntity( + Assignment(id = assignmentId, name = "Test Assignment", assignmentGroupId = 1L, courseId = courseId), + null, null, null, null + ) + assignmentDao.insert(assignmentEntity) + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDaoTest.kt new file mode 100644 index 0000000000..7596709d0b --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDaoTest.kt @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.SubAssignmentSubmission +import com.instructure.canvasapi2.models.Submission +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.AssignmentEntity +import com.instructure.pandautils.room.offline.entities.AssignmentGroupEntity +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.SubAssignmentSubmissionEntity +import com.instructure.pandautils.room.offline.entities.SubmissionEntity +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SubAssignmentSubmissionDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var subAssignmentSubmissionDao: SubAssignmentSubmissionDao + private lateinit var submissionDao: SubmissionDao + private lateinit var assignmentDao: AssignmentDao + private lateinit var assignmentGroupDao: AssignmentGroupDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + subAssignmentSubmissionDao = db.subAssignmentSubmissionDao() + submissionDao = db.submissionDao() + assignmentDao = db.assignmentDao() + assignmentGroupDao = db.assignmentGroupDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertAndFind() = runTest { + setupSubmission(1L, 1L, 1L) + + val subAssignmentSubmission = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "A", + score = 10.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + subAssignmentSubmissionDao.insert(subAssignmentSubmission) + + val result = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 1L) + assertEquals(1, result.size) + assertEquals("A", result[0].grade) + assertEquals(10.0, result[0].score) + assertEquals("reply_to_topic", result[0].subAssignmentTag) + } + + @Test + fun testInsertMultipleSubAssignmentSubmissions() = runTest { + setupSubmission(1L, 1L, 1L) + + val subAssignment1 = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "A", + score = 5.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 5.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + val subAssignment2 = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "B", + score = 4.0, + late = true, + excused = false, + missing = false, + latePolicyStatus = "late", + customGradeStatusId = null, + subAssignmentTag = "reply_to_entry", + enteredScore = 5.0, + enteredGrade = "B", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + subAssignmentSubmissionDao.insertAll(listOf(subAssignment1, subAssignment2)) + + val result = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 1L) + assertEquals(2, result.size) + assertTrue(result.any { it.subAssignmentTag == "reply_to_topic" && it.score == 5.0 }) + assertTrue(result.any { it.subAssignmentTag == "reply_to_entry" && it.late }) + } + + @Test + fun testDeleteBySubmissionIdAndAttempt() = runTest { + setupSubmission(1L, 1L, 1L) + + val subAssignmentSubmission = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "A", + score = 10.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + subAssignmentSubmissionDao.insert(subAssignmentSubmission) + subAssignmentSubmissionDao.deleteBySubmissionIdAndAttempt(1L, 1L) + + val result = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 1L) + assertTrue(result.isEmpty()) + } + + @Test + fun testCascadeDelete() = runTest { + setupSubmission(1L, 1L, 1L) + + val subAssignmentSubmission = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "A", + score = 10.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + subAssignmentSubmissionDao.insert(subAssignmentSubmission) + + val submissions = submissionDao.findById(1L) + val submissionEntity = submissions.first { it.id == 1L && it.attempt == 1L } + submissionDao.delete(submissionEntity) + + val result = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 1L) + assertTrue(result.isEmpty()) + } + + @Test + fun testMultipleAttempts() = runTest { + setupSubmission(1L, 1L, 1L) + + val submission2 = SubmissionEntity( + Submission(id = 1L, assignmentId = 1L, attempt = 2L), + null, + null + ) + submissionDao.insert(submission2) + + val subAssignment1 = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "B", + score = 8.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 8.0, + enteredGrade = "B", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + val subAssignment2 = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 2L, + grade = "A", + score = 10.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + subAssignmentSubmissionDao.insertAll(listOf(subAssignment1, subAssignment2)) + + val attempt1Results = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 1L) + val attempt2Results = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 2L) + + assertEquals(1, attempt1Results.size) + assertEquals(8.0, attempt1Results[0].score) + assertEquals(1, attempt2Results.size) + assertEquals(10.0, attempt2Results[0].score) + } + + @Test + fun testToApiModel() { + val entity = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "A", + score = 10.0, + late = true, + excused = false, + missing = false, + latePolicyStatus = "late", + customGradeStatusId = 123L, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + val apiModel = entity.toApiModel() + + assertEquals("A", apiModel.grade) + assertEquals(10.0, apiModel.score) + assertEquals(true, apiModel.late) + assertEquals(false, apiModel.excused) + assertEquals(false, apiModel.missing) + assertEquals("late", apiModel.latePolicyStatus) + assertEquals(123L, apiModel.customGradeStatusId) + assertEquals("reply_to_topic", apiModel.subAssignmentTag) + assertEquals(10.0, apiModel.enteredScore) + assertEquals("A", apiModel.enteredGrade) + assertEquals(1L, apiModel.userId) + assertEquals(true, apiModel.isGradeMatchesCurrentSubmission) + } + + @Test + fun testConstructorFromApiModel() { + val apiModel = SubAssignmentSubmission( + grade = "A", + score = 10.0, + late = true, + excused = false, + missing = false, + latePolicyStatus = "late", + customGradeStatusId = 123L, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + val entity = SubAssignmentSubmissionEntity(apiModel, 1L, 2L) + + assertEquals(1L, entity.submissionId) + assertEquals(2L, entity.submissionAttempt) + assertEquals("A", entity.grade) + assertEquals(10.0, entity.score) + assertEquals(true, entity.late) + assertEquals("reply_to_topic", entity.subAssignmentTag) + } + + private suspend fun setupSubmission(submissionId: Long, assignmentId: Long, courseId: Long) { + val courseEntity = CourseEntity(Course(id = courseId)) + courseDao.insert(courseEntity) + + val assignmentGroupEntity = AssignmentGroupEntity(AssignmentGroup(id = 1L), courseId) + assignmentGroupDao.insert(assignmentGroupEntity) + + val assignmentEntity = AssignmentEntity( + Assignment(id = assignmentId, name = "Test Assignment", assignmentGroupId = 1L, courseId = courseId), + null, null, null, null + ) + assignmentDao.insert(assignmentEntity) + + val submissionEntity = SubmissionEntity( + Submission(id = submissionId, assignmentId = assignmentId, attempt = 1L), + null, + null + ) + submissionDao.insert(submissionEntity) + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt index 4f4433f3ed..4a2b9f310d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt @@ -30,6 +30,8 @@ import com.instructure.pandautils.room.offline.daos.AssignmentScoreStatisticsDao import com.instructure.pandautils.room.offline.daos.AssignmentSetDao import com.instructure.pandautils.room.offline.daos.AttachmentDao import com.instructure.pandautils.room.offline.daos.AuthorDao +import com.instructure.pandautils.room.offline.daos.CheckpointDao +import com.instructure.pandautils.room.offline.daos.SubAssignmentSubmissionDao import com.instructure.pandautils.room.offline.daos.ConferenceDao import com.instructure.pandautils.room.offline.daos.ConferenceRecodingDao import com.instructure.pandautils.room.offline.daos.CourseDao @@ -318,6 +320,7 @@ class OfflineModule { lockInfoFacade: LockInfoFacade, rubricCriterionRatingDao: RubricCriterionRatingDao, assignmentRubricCriterionDao: AssignmentRubricCriterionDao, + checkpointDao: CheckpointDao, offlineDatabase: OfflineDatabase ): AssignmentFacade { return AssignmentFacade( @@ -332,6 +335,7 @@ class OfflineModule { lockInfoFacade, rubricCriterionRatingDao, assignmentRubricCriterionDao, + checkpointDao, offlineDatabase ) } @@ -345,11 +349,13 @@ class OfflineModule { submissionCommentDao: SubmissionCommentDao, attachmentDao: AttachmentDao, authorDao: AuthorDao, - rubricCriterionAssessmentDao: RubricCriterionAssessmentDao + rubricCriterionAssessmentDao: RubricCriterionAssessmentDao, + subAssignmentSubmissionDao: SubAssignmentSubmissionDao ): SubmissionFacade { return SubmissionFacade( submissionDao, groupDao, mediaCommentDao, userDao, - submissionCommentDao, attachmentDao, authorDao, rubricCriterionAssessmentDao + submissionCommentDao, attachmentDao, authorDao, rubricCriterionAssessmentDao, + subAssignmentSubmissionDao ) } @@ -643,4 +649,14 @@ class OfflineModule { fun provideCustomGradeStatusDao(database: OfflineDatabase): CustomGradeStatusDao { return database.customGradeStatusDao() } + + @Provides + fun provideCheckpointDao(database: OfflineDatabase): CheckpointDao { + return database.checkpointDao() + } + + @Provides + fun provideSubAssignmentSubmissionDao(database: OfflineDatabase): SubAssignmentSubmissionDao { + return database.subAssignmentSubmissionDao() + } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt index 65abd75138..84ae5f008d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt @@ -53,6 +53,7 @@ import com.instructure.pandautils.analytics.SCREEN_VIEW_ASSIGNMENT_DETAILS import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.base.BaseCanvasFragment import com.instructure.pandautils.databinding.FragmentAssignmentDetailsBinding +import com.instructure.pandautils.features.assignments.details.composables.DueDateReminderLayout import com.instructure.pandautils.features.reminder.composables.ReminderView import com.instructure.pandautils.features.shareextension.ShareFileSubmissionTarget import com.instructure.pandautils.navigation.WebViewRouter @@ -146,16 +147,22 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo viewModel.course.value?.let { viewModel.updateReminderColor(assignmentDetailsBehaviour.getThemeColor(it)) } - binding?.reminderComposeView?.setContent { - val state by viewModel.reminderViewState.collectAsState() - ReminderView( - viewState = state, - onAddClick = { checkAlarmPermission() }, + + binding?.dueComposeView?.setContent { + val states = viewModel.dueDateReminderViewStates + DueDateReminderLayout( + states, + onAddClick = { tag -> checkAlarmPermission(tag) }, onRemoveClick = { reminderId -> - viewModel.showDeleteReminderConfirmationDialog(requireContext(), reminderId, assignmentDetailsBehaviour.dialogColor) + viewModel.showDeleteReminderConfirmationDialog( + requireContext(), + reminderId, + assignmentDetailsBehaviour.dialogColor + ) } ) } + return binding?.root } @@ -367,14 +374,14 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo } } - private fun checkAlarmPermission() { + private fun checkAlarmPermission(tag: String? = null) { val alarmManager = context?.getSystemService(Context.ALARM_SERVICE) as AlarmManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && requireActivity().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { viewModel.checkingNotificationPermission = true notificationsPermissionContract.launch(Manifest.permission.POST_NOTIFICATIONS) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (alarmManager.canScheduleExactAlarms()) { - viewModel.showCreateReminderDialog(requireActivity(), assignmentDetailsBehaviour.dialogColor) + viewModel.showCreateReminderDialog(requireActivity(), assignmentDetailsBehaviour.dialogColor, tag) } else { viewModel.checkingReminderPermission = true startActivity( @@ -385,7 +392,7 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo ) } } else { - viewModel.showCreateReminderDialog(requireActivity(), assignmentDetailsBehaviour.dialogColor) + viewModel.showCreateReminderDialog(requireActivity(), assignmentDetailsBehaviour.dialogColor, tag) } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewData.kt index c5a904087d..a91907067a 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewData.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewData.kt @@ -40,8 +40,7 @@ data class AssignmentDetailsViewData( val discussionHeaderViewData: DiscussionHeaderViewData? = null, val quizDetails: QuizViewViewData? = null, val attemptsViewData: AttemptsViewData? = null, - @Bindable var hasDraft: Boolean = false, - @Bindable var reminders: List = emptyList() + @Bindable var hasDraft: Boolean = false ) : BaseObservable() { val firstAttemptOrNull = attempts.firstOrNull() val noDescriptionVisible = description.isEmpty() && !fullLocked diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt index 654d3c0392..fcbe41d10b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt @@ -22,6 +22,7 @@ import android.content.Context import android.content.res.Resources import android.net.Uri import androidx.annotation.ColorInt +import androidx.compose.runtime.mutableStateListOf import androidx.compose.ui.graphics.Color import androidx.fragment.app.FragmentActivity import androidx.lifecycle.LiveData @@ -69,6 +70,7 @@ import com.instructure.pandautils.utils.HtmlContentFormatter import com.instructure.pandautils.utils.getSubmissionStateLabel import com.instructure.pandautils.utils.isAudioVisualExtension import com.instructure.pandautils.utils.orDefault +import com.instructure.pandautils.utils.orderedCheckpoints import com.instructure.pandautils.utils.toFormattedString import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -134,8 +136,11 @@ class AssignmentDetailsViewModel @Inject constructor( private var selectedSubmission: Submission? = null - private val _reminderViewState = MutableStateFlow(ReminderViewState()) - val reminderViewState = _reminderViewState.asStateFlow() + private var reminderEntities: List = emptyList() + private var themeColor: Color? = null + private val _dueDateReminderViewStates = mutableStateListOf() + val dueDateReminderViewStates: List + get() = _dueDateReminderViewStates var checkingReminderPermission = false var checkingNotificationPermission = false @@ -156,15 +161,29 @@ class AssignmentDetailsViewModel @Inject constructor( loadData() reminderManager.observeRemindersLiveData(apiPrefs.user?.id.orDefault(), assignmentId) { reminderEntities -> - _data.value?.reminders = mapReminders(reminderEntities) - _reminderViewState.update { it.copy( - reminders = reminderEntities.map { ReminderItem(it.id, it.text, Date(it.time)) }, - dueDate = assignment?.dueDate - ) } - _data.value?.notifyPropertyChanged(BR.reminders) + this.reminderEntities = reminderEntities + updateDueDatesViewState(reminderEntities) } } + private fun updateDueDatesViewState(reminderEntities: List) { + for (i in 0.._dueDateReminderViewStates.lastIndex) { + val tag = _dueDateReminderViewStates[i].tag + _dueDateReminderViewStates[i] = _dueDateReminderViewStates[i].copy( + reminders = getReminderItems(tag) + ) + } + } + + private fun getReminderItems(tag: String? = null): List { + return reminderEntities + .filter { it.tag == tag } + .sortedBy { it.time } + .map { + ReminderItem(it.id, it.text, Date(it.time)) + } + } + fun getVideoUri(fragment: FragmentActivity): Uri? = submissionHandler.getVideoUri(fragment) override fun onCleared() { @@ -227,9 +246,44 @@ class AssignmentDetailsViewModel @Inject constructor( isAssignmentEnhancementEnabled = assignmentDetailsRepository.isAssignmentEnhancementEnabled(courseId.orDefault(), forceNetwork) assignment = assignmentResult - _reminderViewState.update { it.copy( - dueDate = if (assignment?.submission?.excused.orDefault()) null else assignment?.dueDate - ) } + + if (assignment?.checkpoints?.isNotEmpty() == true) { + _dueDateReminderViewStates.clear() + assignment?.orderedCheckpoints?.forEach { checkpoint -> + val dueLabel = when (checkpoint.tag) { + Const.REPLY_TO_TOPIC -> application.getString(R.string.reply_to_topic_due) + Const.REPLY_TO_ENTRY -> { + application.getString( + R.string.additional_replies_due, + assignment?.discussionTopicHeader?.replyRequiredCount ?: 0 + ) + } + + else -> application.getString(R.string.dueLabel) + } + val subAssignment = assignment?.submission?.subAssignmentSubmissions?.firstOrNull { it.subAssignmentTag == checkpoint.tag } + _dueDateReminderViewStates.add( + ReminderViewState( + dueLabel = dueLabel, + themeColor = themeColor, + dueDate = if (subAssignment?.excused.orDefault()) null else checkpoint.dueDate, + tag = checkpoint.tag, + reminders = getReminderItems(checkpoint.tag) + ) + ) + } + } else { + _dueDateReminderViewStates.clear() + _dueDateReminderViewStates.add( + ReminderViewState( + dueLabel = application.getString(R.string.dueLabel), + themeColor = themeColor, + dueDate = if (assignment?.submission?.excused.orDefault()) null else assignment?.dueDate, + tag = null, + reminders = getReminderItems() + ) + ) + } _data.postValue(getViewData(assignmentResult, hasDraft)) _state.postValue(ViewState.Success) @@ -478,8 +532,7 @@ class AssignmentDetailsViewModel @Inject constructor( discussionHeaderViewData = discussionHeaderViewData, quizDetails = quizViewViewData, attemptsViewData = attemptsViewData, - hasDraft = hasDraft, - reminders = _data.value?.reminders.orEmpty(), + hasDraft = hasDraft ) } @@ -628,28 +681,34 @@ class AssignmentDetailsViewModel @Inject constructor( } fun updateReminderColor(@ColorInt color: Int) { - _reminderViewState.update { it.copy(themeColor = Color(color)) } + themeColor = Color(color) + for (i in 0.._dueDateReminderViewStates.lastIndex) { + _dueDateReminderViewStates[i] = _dueDateReminderViewStates[i].copy(themeColor = themeColor) + } } - fun showCreateReminderDialog(context: Context, @ColorInt color: Int) { + fun showCreateReminderDialog(context: Context, @ColorInt color: Int, tag: String? = null) { assignment?.let { assignment -> viewModelScope.launch { + val dueDate = _dueDateReminderViewStates.firstOrNull { it.tag == tag }?.dueDate when { - assignment.dueDate == null -> reminderManager.showCustomReminderDialog( + dueDate == null -> reminderManager.showCustomReminderDialog( context, apiPrefs.user?.id.orDefault(), assignment.id, assignment.name.orEmpty(), assignment.htmlUrl.orEmpty(), - assignment.dueDate + dueDate, + tag ) - assignment.dueDate?.before(Date()).orDefault() -> reminderManager.showCustomReminderDialog( + dueDate.before(Date()).orDefault() -> reminderManager.showCustomReminderDialog( context, apiPrefs.user?.id.orDefault(), assignment.id, assignment.name.orEmpty(), assignment.htmlUrl.orEmpty(), - assignment.dueDate + dueDate, + tag ) else -> reminderManager.showBeforeDueDateReminderDialog( context, @@ -657,8 +716,9 @@ class AssignmentDetailsViewModel @Inject constructor( assignment.id, assignment.name.orEmpty(), assignment.htmlUrl.orEmpty(), - assignment.dueDate ?: Date(), - color + dueDate ?: Date(), + color, + tag ) } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DueDateReminderLayout.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DueDateReminderLayout.kt new file mode 100644 index 0000000000..b7c7705ac0 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DueDateReminderLayout.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.pandautils.features.assignments.details.composables + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.instructure.pandares.R +import com.instructure.pandautils.compose.composables.CanvasDivider +import com.instructure.pandautils.features.reminder.ReminderViewState +import com.instructure.pandautils.features.reminder.composables.ReminderView +import com.instructure.pandautils.utils.toFormattedString + +@Composable +fun DueDateReminderLayout( + reminderViewStates: List, + onAddClick: (String?) -> Unit, + onRemoveClick: (Long) -> Unit, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + reminderViewStates.forEachIndexed { index, reminderViewState -> + DueDateBlock(reminderViewState, index) + ReminderView( + viewState = reminderViewState, + onAddClick = onAddClick, + onRemoveClick = onRemoveClick, + modifier = Modifier.testTag("reminderView-$index") + ) + CanvasDivider() + } + } +} + +@Composable +private fun DueDateBlock( + reminderViewState: ReminderViewState, + position: Int +) { + Text( + modifier = Modifier + .padding(top = 24.dp, start = 16.dp, end = 16.dp) + .semantics { heading() } + .testTag("dueDateHeaderText-$position"), + text = reminderViewState.dueLabel ?: stringResource(id = R.string.dueLabel), + color = colorResource(id = R.color.textDark), + fontSize = 14.sp + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + modifier = Modifier + .padding(bottom = 14.dp, start = 16.dp, end = 16.dp) + .testTag("dueDateText-$position"), + text = reminderViewState.dueDate?.toFormattedString() ?: stringResource(R.string.toDoNoDueDate), + color = colorResource(id = R.color.textDarkest), + fontSize = 16.sp + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderManager.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderManager.kt index b82889fb91..bf2d3d48d8 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderManager.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderManager.kt @@ -53,17 +53,18 @@ class ReminderManager( contentName: String, contentHtmlUrl: String, dueDate: Date, - @ColorInt color: Int + @ColorInt color: Int, + tag: String? = null ) { showBeforeDueDateReminderDialog(context, dueDate, color).collect { calendar -> - createReminder(context, calendar, userId, contentId, contentName, contentHtmlUrl, dueDate) + createReminder(context, calendar, userId, contentId, contentName, contentHtmlUrl, dueDate, tag) } } private fun showBeforeDueDateReminderDialog( context: Context, dueDate: Date, - @ColorInt color: Int, + @ColorInt color: Int ) = callbackFlow { val choices = listOf( ReminderChoice.Minute(5), @@ -115,10 +116,11 @@ class ReminderManager( contentId: Long, contentName: String, contentHtmlUrl: String, - dueDate: Date? + dueDate: Date?, + tag: String? = null ) { showCustomReminderDialog(context).collect { calendar -> - createReminder(context, calendar, userId, contentId, contentName, contentHtmlUrl, dueDate) + createReminder(context, calendar, userId, contentId, contentName, contentHtmlUrl, dueDate, tag) } } @@ -185,7 +187,8 @@ class ReminderManager( contentId: Long, contentName: String, contentHtmlUrl: String, - dueDate: Date? + dueDate: Date?, + tag: String? = null ) { val alarmTimeInMillis = calendar.timeInMillis if (reminderRepository.isReminderAlreadySetForTime(userId, contentId, calendar.timeInMillis)) { @@ -220,7 +223,8 @@ class ReminderManager( contentHtmlUrl, reminderTitle, reminderMessage, - alarmTimeInMillis + alarmTimeInMillis, + tag ) } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderRepository.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderRepository.kt index f626810df0..c44ba84457 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderRepository.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderRepository.kt @@ -32,7 +32,8 @@ class ReminderRepository( contentHtmlUrl: String, title: String, alarmText: String, - alarmTimeInMillis: Long + alarmTimeInMillis: Long, + tag: String? = null ) { val reminder = ReminderEntity( userId = userId, @@ -40,7 +41,8 @@ class ReminderRepository( name = title, htmlUrl = contentHtmlUrl, text = Date(alarmTimeInMillis).toFormattedString(), - time = alarmTimeInMillis + time = alarmTimeInMillis, + tag = tag ) val reminderId = reminderDao.insert(reminder) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderViewState.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderViewState.kt index ff311088c8..ef346fa174 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderViewState.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderViewState.kt @@ -9,6 +9,8 @@ data class ReminderViewState( val reminders: List = emptyList(), val dueDate: Date? = null, val themeColor: Color? = null, + val dueLabel: String? = null, + val tag: String? = null ) { fun getThemeColor(context: Context): Color { return themeColor ?: Color(context.getColor(R.color.textDarkest)) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/composables/ReminderView.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/composables/ReminderView.kt index 3a90036497..b32df8d3b7 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/composables/ReminderView.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/composables/ReminderView.kt @@ -51,12 +51,13 @@ import com.instructure.pandautils.utils.toFormattedString @Composable fun ReminderView( viewState: ReminderViewState, - onAddClick: () -> Unit, + modifier: Modifier = Modifier, + onAddClick: (String?) -> Unit, onRemoveClick: (Long) -> Unit, ) { CanvasTheme { Column( - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding(vertical = 24.dp, horizontal = 16.dp) ) { @@ -71,9 +72,11 @@ fun ReminderView( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .padding(vertical = 12.dp) - .clickable { onAddClick() } + .clickable { + onAddClick(viewState.tag) + } ) { - IconButton(onClick = { onAddClick() }) { + IconButton(onClick = { onAddClick(viewState.tag) }) { Icon( painter = painterResource(id = R.drawable.ic_add), contentDescription = stringResource(id = R.string.a11y_addReminder), diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabase.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabase.kt index 7b0e9b34fa..0f75d10ff9 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabase.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabase.kt @@ -44,7 +44,7 @@ import com.instructure.pandautils.room.common.Converters ModuleBulkProgressEntity::class, AssignmentListSelectedFiltersEntity::class, FileDownloadProgressEntity::class - ], version = 12 + ], version = 13 ) @TypeConverters(Converters::class, AssignmentFilterConverter::class) abstract class AppDatabase : RoomDatabase() { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabaseMigrations.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabaseMigrations.kt index b5f427495b..bcdd9e678b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabaseMigrations.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabaseMigrations.kt @@ -69,5 +69,9 @@ val appDatabaseMigrations = arrayOf( createMigration(11, 12) { database -> database.execSQL("CREATE TABLE IF NOT EXISTS FileDownloadProgressEntity (workerId TEXT NOT NULL, fileName TEXT NOT NULL, progress INTEGER NOT NULL, progressState TEXT NOT NULL, filePath TEXT NOT NULL, PRIMARY KEY(workerId))") - } + }, + + createMigration(12, 13) { database -> + database.execSQL("ALTER TABLE ReminderEntity ADD COLUMN tag TEXT") + }, ) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/ReminderEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/ReminderEntity.kt index e98f494de8..ed14028908 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/ReminderEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/ReminderEntity.kt @@ -30,5 +30,6 @@ data class ReminderEntity( val htmlUrl: String, val name: String, val text: String, - val time: Long + val time: Long, + val tag: String? = null ) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt index fbc3704cd9..ff10789041 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt @@ -29,6 +29,8 @@ import com.instructure.pandautils.room.offline.daos.AssignmentScoreStatisticsDao import com.instructure.pandautils.room.offline.daos.AssignmentSetDao import com.instructure.pandautils.room.offline.daos.AttachmentDao import com.instructure.pandautils.room.offline.daos.AuthorDao +import com.instructure.pandautils.room.offline.daos.CheckpointDao +import com.instructure.pandautils.room.offline.daos.SubAssignmentSubmissionDao import com.instructure.pandautils.room.offline.daos.ConferenceDao import com.instructure.pandautils.room.offline.daos.ConferenceRecodingDao import com.instructure.pandautils.room.offline.daos.CourseDao @@ -93,7 +95,9 @@ import com.instructure.pandautils.room.offline.entities.AssignmentScoreStatistic import com.instructure.pandautils.room.offline.entities.AssignmentSetEntity import com.instructure.pandautils.room.offline.entities.AttachmentEntity import com.instructure.pandautils.room.offline.entities.AuthorEntity +import com.instructure.pandautils.room.offline.entities.CheckpointEntity import com.instructure.pandautils.room.offline.entities.ConferenceEntity +import com.instructure.pandautils.room.offline.entities.SubAssignmentSubmissionEntity import com.instructure.pandautils.room.offline.entities.ConferenceRecordingEntity import com.instructure.pandautils.room.offline.entities.CourseEntity import com.instructure.pandautils.room.offline.entities.CourseFeaturesEntity @@ -226,8 +230,10 @@ import com.instructure.pandautils.room.offline.entities.UserEntity CourseSyncProgressEntity::class, FileSyncProgressEntity::class, StudioMediaProgressEntity::class, - CustomGradeStatusEntity::class - ], version = 5 + CustomGradeStatusEntity::class, + CheckpointEntity::class, + SubAssignmentSubmissionEntity::class + ], version = 6 ) @TypeConverters(value = [Converters::class, OfflineConverters::class]) abstract class OfflineDatabase : RoomDatabase() { @@ -357,4 +363,8 @@ abstract class OfflineDatabase : RoomDatabase() { abstract fun studioMediaProgressDao(): StudioMediaProgressDao abstract fun customGradeStatusDao(): CustomGradeStatusDao + + abstract fun checkpointDao(): CheckpointDao + + abstract fun subAssignmentSubmissionDao(): SubAssignmentSubmissionDao } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt index baccad242f..5b09de227b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt @@ -119,5 +119,41 @@ val offlineDatabaseMigrations = arrayOf( "PRIMARY KEY(`id`, `courseId`)," + "FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE)" ) + }, + createMigration(5, 6) { database -> + database.execSQL("ALTER TABLE `SubmissionEntity` ADD COLUMN `hasSubAssignmentSubmissions` INTEGER NOT NULL DEFAULT 0") + database.execSQL("ALTER TABLE `DiscussionTopicHeaderEntity` ADD COLUMN `replyRequiredCount` INTEGER") + database.execSQL( + "CREATE TABLE IF NOT EXISTS `CheckpointEntity` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," + + "`assignmentId` INTEGER NOT NULL," + + "`name` TEXT," + + "`tag` TEXT," + + "`pointsPossible` REAL," + + "`dueAt` TEXT," + + "`onlyVisibleToOverrides` INTEGER NOT NULL," + + "`lockAt` TEXT," + + "`unlockAt` TEXT," + + "FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE)" + ) + database.execSQL( + "CREATE TABLE IF NOT EXISTS `SubAssignmentSubmissionEntity` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," + + "`submissionId` INTEGER NOT NULL," + + "`submissionAttempt` INTEGER NOT NULL," + + "`grade` TEXT," + + "`score` REAL NOT NULL," + + "`late` INTEGER NOT NULL," + + "`excused` INTEGER NOT NULL," + + "`missing` INTEGER NOT NULL," + + "`latePolicyStatus` TEXT," + + "`customGradeStatusId` INTEGER," + + "`subAssignmentTag` TEXT," + + "`enteredScore` REAL NOT NULL," + + "`enteredGrade` TEXT," + + "`userId` INTEGER NOT NULL," + + "`isGradeMatchesCurrentSubmission` INTEGER NOT NULL," + + "FOREIGN KEY(`submissionId`, `submissionAttempt`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE)" + ) } ) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CheckpointDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CheckpointDao.kt new file mode 100644 index 0000000000..5ed1e092de --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CheckpointDao.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.instructure.pandautils.room.offline.entities.CheckpointEntity + +@Dao +interface CheckpointDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: CheckpointEntity): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List) + + @Query("SELECT * FROM CheckpointEntity WHERE assignmentId = :assignmentId") + suspend fun findByAssignmentId(assignmentId: Long): List + + @Query("DELETE FROM CheckpointEntity WHERE assignmentId = :assignmentId") + suspend fun deleteByAssignmentId(assignmentId: Long) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDao.kt new file mode 100644 index 0000000000..f3c6870a5d --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDao.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.instructure.pandautils.room.offline.entities.SubAssignmentSubmissionEntity + +@Dao +interface SubAssignmentSubmissionDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: SubAssignmentSubmissionEntity): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List) + + @Query("SELECT * FROM SubAssignmentSubmissionEntity WHERE submissionId = :submissionId AND submissionAttempt = :submissionAttempt") + suspend fun findBySubmissionIdAndAttempt(submissionId: Long, submissionAttempt: Long): List + + @Query("DELETE FROM SubAssignmentSubmissionEntity WHERE submissionId = :submissionId AND submissionAttempt = :submissionAttempt") + suspend fun deleteBySubmissionIdAndAttempt(submissionId: Long, submissionAttempt: Long) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentEntity.kt index 5cb18f0e4a..ca43674b85 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentEntity.kt @@ -134,7 +134,8 @@ data class AssignmentEntity( lockInfo: LockInfo? = null, discussionTopicHeader: DiscussionTopicHeader? = null, scoreStatistics: AssignmentScoreStatistics? = null, - plannerOverride: PlannerOverride? = null + plannerOverride: PlannerOverride? = null, + checkpoints: List = emptyList() ) = Assignment( id = id, name = name, @@ -186,6 +187,7 @@ data class AssignmentEntity( inClosedGradingPeriod = inClosedGradingPeriod, annotatableAttachmentId = annotatableAttachmentId, anonymousSubmissions = anonymousSubmissions, - omitFromFinalGrade = omitFromFinalGrade + omitFromFinalGrade = omitFromFinalGrade, + checkpoints = checkpoints ) } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CheckpointEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CheckpointEntity.kt new file mode 100644 index 0000000000..52ea48ba73 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CheckpointEntity.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Checkpoint + +@Entity( + foreignKeys = [ + ForeignKey( + entity = AssignmentEntity::class, + parentColumns = ["id"], + childColumns = ["assignmentId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class CheckpointEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val assignmentId: Long, + val name: String?, + val tag: String?, + val pointsPossible: Double?, + val dueAt: String?, + val onlyVisibleToOverrides: Boolean, + val lockAt: String?, + val unlockAt: String? +) { + constructor(checkpoint: Checkpoint, assignmentId: Long) : this( + assignmentId = assignmentId, + name = checkpoint.name, + tag = checkpoint.tag, + pointsPossible = checkpoint.pointsPossible, + dueAt = checkpoint.dueAt, + onlyVisibleToOverrides = checkpoint.onlyVisibleToOverrides, + lockAt = checkpoint.lockAt, + unlockAt = checkpoint.unlockAt + ) + + fun toApiModel() = Checkpoint( + name = name, + tag = tag, + pointsPossible = pointsPossible, + dueAt = dueAt, + overrides = null, + onlyVisibleToOverrides = onlyVisibleToOverrides, + lockAt = lockAt, + unlockAt = unlockAt + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicHeaderEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicHeaderEntity.kt index 4b26468d14..7e551cc76b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicHeaderEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicHeaderEntity.kt @@ -86,7 +86,8 @@ data class DiscussionTopicHeaderEntity( var lockAt: Date?, var userCanSeePosts: Boolean, var specificSections: String?, - var anonymousState: String? + var anonymousState: String?, + var replyRequiredCount: Int? ) { constructor(discussionTopicHeader: DiscussionTopicHeader, courseId: Long, permissionId: Long? = null) : this( discussionTopicHeader.id, @@ -121,7 +122,8 @@ data class DiscussionTopicHeaderEntity( discussionTopicHeader.lockAt, discussionTopicHeader.userCanSeePosts, discussionTopicHeader.specificSections, - discussionTopicHeader.anonymousState + discussionTopicHeader.anonymousState, + discussionTopicHeader.replyRequiredCount ) fun toApiModel( @@ -170,5 +172,6 @@ data class DiscussionTopicHeaderEntity( //TODO sections = null, anonymousState = anonymousState, + replyRequiredCount = replyRequiredCount ) } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubAssignmentSubmissionEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubAssignmentSubmissionEntity.kt new file mode 100644 index 0000000000..cb40b005d0 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubAssignmentSubmissionEntity.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.SubAssignmentSubmission + +@Entity( + foreignKeys = [ + ForeignKey( + entity = SubmissionEntity::class, + parentColumns = ["id", "attempt"], + childColumns = ["submissionId", "submissionAttempt"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class SubAssignmentSubmissionEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val submissionId: Long, + val submissionAttempt: Long, + val grade: String?, + val score: Double, + val late: Boolean, + val excused: Boolean, + val missing: Boolean, + val latePolicyStatus: String?, + val customGradeStatusId: Long?, + val subAssignmentTag: String?, + val enteredScore: Double, + val enteredGrade: String?, + val userId: Long, + val isGradeMatchesCurrentSubmission: Boolean +) { + constructor(subAssignmentSubmission: SubAssignmentSubmission, submissionId: Long, submissionAttempt: Long) : this( + submissionId = submissionId, + submissionAttempt = submissionAttempt, + grade = subAssignmentSubmission.grade, + score = subAssignmentSubmission.score, + late = subAssignmentSubmission.late, + excused = subAssignmentSubmission.excused, + missing = subAssignmentSubmission.missing, + latePolicyStatus = subAssignmentSubmission.latePolicyStatus, + customGradeStatusId = subAssignmentSubmission.customGradeStatusId, + subAssignmentTag = subAssignmentSubmission.subAssignmentTag, + enteredScore = subAssignmentSubmission.enteredScore, + enteredGrade = subAssignmentSubmission.enteredGrade, + userId = subAssignmentSubmission.userId, + isGradeMatchesCurrentSubmission = subAssignmentSubmission.isGradeMatchesCurrentSubmission + ) + + fun toApiModel() = SubAssignmentSubmission( + grade = grade, + score = score, + late = late, + excused = excused, + missing = missing, + latePolicyStatus = latePolicyStatus, + customGradeStatusId = customGradeStatusId, + subAssignmentTag = subAssignmentTag, + enteredScore = enteredScore, + enteredGrade = enteredGrade, + userId = userId, + isGradeMatchesCurrentSubmission = isGradeMatchesCurrentSubmission + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionEntity.kt index 2467a887c2..ed2faa4092 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionEntity.kt @@ -24,6 +24,7 @@ import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.models.MediaComment import com.instructure.canvasapi2.models.RubricCriterionAssessment +import com.instructure.canvasapi2.models.SubAssignmentSubmission import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.models.SubmissionComment import com.instructure.canvasapi2.models.User @@ -83,7 +84,8 @@ data class SubmissionEntity( val enteredGrade: String?, val postedAt: Date?, val gradingPeriodId: Long?, - val customGradeStatusId: Long? + val customGradeStatusId: Long?, + val hasSubAssignmentSubmissions: Boolean ) { constructor(submission: Submission, groupId: Long?, mediaCommentId: String?) : this( id = submission.id, @@ -114,7 +116,8 @@ data class SubmissionEntity( enteredGrade = submission.enteredGrade, postedAt = submission.postedAt, gradingPeriodId = submission.gradingPeriodId, - customGradeStatusId = submission.customGradeStatusId + customGradeStatusId = submission.customGradeStatusId, + hasSubAssignmentSubmissions = submission.hasSubAssignmentSubmissions ) fun toApiModel( @@ -125,7 +128,8 @@ data class SubmissionEntity( mediaComment: MediaComment? = null, assignment: Assignment? = null, user: User? = null, - group: Group? = null + group: Group? = null, + subAssignmentSubmissions: ArrayList = arrayListOf() ) = Submission( id = id, grade = grade, @@ -163,6 +167,8 @@ data class SubmissionEntity( enteredGrade = enteredGrade, postedAt = postedAt, gradingPeriodId = gradingPeriodId, - customGradeStatusId = customGradeStatusId + customGradeStatusId = customGradeStatusId, + hasSubAssignmentSubmissions = hasSubAssignmentSubmissions, + subAssignmentSubmissions = subAssignmentSubmissions ) } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/AssignmentFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/AssignmentFacade.kt index 888ac63d26..9b8ddeb8a7 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/AssignmentFacade.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/AssignmentFacade.kt @@ -37,6 +37,7 @@ class AssignmentFacade( private val lockInfoFacade: LockInfoFacade, private val rubricCriterionRatingDao: RubricCriterionRatingDao, private val assignmentRubricCriterionDao: AssignmentRubricCriterionDao, + private val checkpointDao: CheckpointDao, private val offlineDatabase: OfflineDatabase ) { @@ -94,6 +95,10 @@ class AssignmentFacade( assignment.lockInfo?.let { lockInfoFacade.insertLockInfoForAssignment(it, assignment.id) } + + checkpointDao.insertAll(assignment.checkpoints.map { + CheckpointEntity(it, assignment.id) + }) } private suspend fun insertPlannerOverride(plannerOverride: PlannerOverride?): Long? { @@ -132,6 +137,7 @@ class AssignmentFacade( val rubricCriterionEntities = assignmentRubricCriterionDao.findByAssignmentId(assignmentEntity.id).mapNotNull { rubricCriterionDao.findById(it.rubricId) } + val checkpointEntities = checkpointDao.findByAssignmentId(assignmentEntity.id) return assignmentEntity.toApiModel( rubric = rubricCriterionEntities.map { rubricCriterionEntity -> @@ -143,7 +149,8 @@ class AssignmentFacade( lockInfo = lockInfo, discussionTopicHeader = discussionTopicHeader, scoreStatistics = scoreStatisticsEntity?.toApiModel(), - plannerOverride = plannerOverrideEntity?.toApiModel() + plannerOverride = plannerOverrideEntity?.toApiModel(), + checkpoints = checkpointEntities.map { it.toApiModel() } ).apply { /* * the assignment model has a submission that contains the assignment, but the inner assignment model cannot diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/SubmissionFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/SubmissionFacade.kt index 971e76e94c..a946de391a 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/SubmissionFacade.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/SubmissionFacade.kt @@ -32,7 +32,8 @@ class SubmissionFacade( private val submissionCommentDao: SubmissionCommentDao, private val attachmentDao: AttachmentDao, private val authorDao: AuthorDao, - private val rubricCriterionAssessmentDao: RubricCriterionAssessmentDao + private val rubricCriterionAssessmentDao: RubricCriterionAssessmentDao, + private val subAssignmentSubmissionDao: SubAssignmentSubmissionDao ) { suspend fun insertSubmission(submission: Submission) { @@ -75,6 +76,10 @@ class SubmissionFacade( submission.submissionHistory.forEach { submissionHistoryItem -> submissionHistoryItem?.let { insertSubmission(it) } } + + subAssignmentSubmissionDao.insertAll(submission.subAssignmentSubmissions.map { + SubAssignmentSubmissionEntity(it, submission.id, submission.attempt) + }) } suspend fun getSubmissionById(id: Long): Submission? { @@ -93,6 +98,7 @@ class SubmissionFacade( val submissionCommentEntities = submissionCommentDao.findBySubmissionId(submissionEntity.id) val attachmentEntities = attachmentDao.findBySubmissionId(submissionEntity.id) val rubricCriterionAssessmentEntities = rubricCriterionAssessmentDao.findByAssignmentId(submissionEntity.assignmentId) + val subAssignmentSubmissionEntities = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(submissionEntity.id, submissionEntity.attempt) return submissionEntity.toApiModel( mediaComment = mediaCommentEntity?.toApiModel(), @@ -100,7 +106,8 @@ class SubmissionFacade( group = groupEntity?.toApiModel(), submissionComments = submissionCommentEntities.map { it.toApiModel() }, attachments = attachmentEntities.filter { it.attempt == submissionEntity.attempt }.map { it.toApiModel() }, - rubricAssessment = HashMap(rubricCriterionAssessmentEntities.associateBy({ it.id }, { it.toApiModel() })) + rubricAssessment = HashMap(rubricCriterionAssessmentEntities.associateBy({ it.id }, { it.toApiModel() })), + subAssignmentSubmissions = ArrayList(subAssignmentSubmissionEntities.map { it.toApiModel() }) ) } diff --git a/libs/pandautils/src/main/res/layout/fragment_assignment_details.xml b/libs/pandautils/src/main/res/layout/fragment_assignment_details.xml index e7b42ce6cf..e15e3e0b33 100644 --- a/libs/pandautils/src/main/res/layout/fragment_assignment_details.xml +++ b/libs/pandautils/src/main/res/layout/fragment_assignment_details.xml @@ -268,7 +268,7 @@ android:visibility="@{viewModel.data.fullLocked ? View.VISIBLE : View.GONE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/reminderBottomDivider" /> + app:layout_constraintTop_toBottomOf="@id/dueComposeView" /> - - - - - - - - + app:layout_constraintTop_toBottomOf="@id/dueDivider" /> + app:layout_constraintTop_toBottomOf="@id/dueComposeView" /> "${(call.invocation.args[1] as Array<*>)[0]} Before" } + + val course = + Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + val checkpoint1 = Checkpoint( + tag = "reply_to_topic", + dueAt = Calendar.getInstance() + .apply { add(Calendar.DAY_OF_MONTH, 1) }.time.toApiString() + ) + val checkpoint2 = Checkpoint( + tag = "reply_to_entry", + dueAt = Calendar.getInstance() + .apply { add(Calendar.DAY_OF_MONTH, 2) }.time.toApiString() + ) + + val subSubmission1 = SubAssignmentSubmission( + subAssignmentTag = "reply_to_topic", + + ) + val subSubmission2 = SubAssignmentSubmission(subAssignmentTag = "reply_to_entry") + + val assignment = Assignment( + checkpoints = listOf(checkpoint1, checkpoint2), + submission = Submission( + subAssignmentSubmissions = arrayListOf(subSubmission1, subSubmission2) + ) + ) + coEvery { + assignmentDetailsRepository.getAssignment( + any(), + any(), + any(), + any() + ) + } returns assignment + + val viewModel = getViewModel(realReminderManager) + + assertEquals( + reminderEntities.filter { it.tag == "reply_to_topic" }.map { it.id }, + viewModel.dueDateReminderViewStates[0].reminders.map { it.id } + ) + + assertEquals( + reminderEntities.filter { it.tag == "reply_to_entry" }.map { it.id }, + viewModel.dueDateReminderViewStates[1].reminders.map { it.id } ) } @@ -823,11 +889,80 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel(realReminderManager) - assertEquals(0, viewModel.data.value?.reminders?.size) + assertEquals(0, viewModel.dueDateReminderViewStates[0].reminders.size) remindersLiveData.value = listOf(ReminderEntity(1, 1, 1, "htmlUrl1", "Assignment 1", "1 day", 1000)) - assertEquals(ReminderViewData(1, "1 day"), viewModel.data.value?.reminders?.first()?.data) + assertEquals( + listOf(1L), + viewModel.dueDateReminderViewStates[0].reminders.map { it.id } + ) + } + + @Test + fun `Reminders update correctly for discussion checkpoints`() { + val remindersLiveData = MutableLiveData>() + val dateTimePicker: DateTimePicker = mockk(relaxed = true) + val reminderRepository: ReminderRepository = mockk(relaxed = true) + val realReminderManager = ReminderManager(dateTimePicker, reminderRepository, analytics) + every { reminderRepository.findByAssignmentIdLiveData(any(), any()) } returns remindersLiveData + every { resources.getString(eq(R.string.reminderBefore), any()) } answers { call -> "${(call.invocation.args[1] as Array<*>)[0]} Before" } + + val course = + Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + val checkpoint1 = Checkpoint( + tag = "reply_to_topic", + dueAt = Calendar.getInstance() + .apply { add(Calendar.DAY_OF_MONTH, 1) }.time.toApiString() + ) + val checkpoint2 = Checkpoint( + tag = "reply_to_entry", + dueAt = Calendar.getInstance() + .apply { add(Calendar.DAY_OF_MONTH, 2) }.time.toApiString() + ) + + val subSubmission1 = SubAssignmentSubmission( + subAssignmentTag = "reply_to_topic", + + ) + val subSubmission2 = SubAssignmentSubmission(subAssignmentTag = "reply_to_entry") + + val assignment = Assignment( + checkpoints = listOf(checkpoint1, checkpoint2), + submission = Submission( + subAssignmentSubmissions = arrayListOf(subSubmission1, subSubmission2) + ) + ) + coEvery { + assignmentDetailsRepository.getAssignment( + any(), + any(), + any(), + any() + ) + } returns assignment + + val viewModel = getViewModel(realReminderManager) + + assertEquals(0, viewModel.dueDateReminderViewStates[0].reminders.size) + assertEquals(0, viewModel.dueDateReminderViewStates[1].reminders.size) + + remindersLiveData.value = listOf( + ReminderEntity(1, 1, 1, "htmlUrl1", "Assignment 1", "1 day", 1000, "reply_to_topic"), + ReminderEntity(2, 1, 1, "htmlUrl1", "Assignment 1", "2 day", 2000, "reply_to_entry") + ) + + assertEquals( + listOf(1L), + viewModel.dueDateReminderViewStates[0].reminders.map { it.id } + ) + + assertEquals( + listOf(2L), + viewModel.dueDateReminderViewStates[1].reminders.map { it.id } + ) } @Test @@ -1121,4 +1256,53 @@ class AssignmentDetailsViewModelTest { assertFalse(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToSubmissionScreen) } + + @Test + fun `Assignment with checkpoints and subAssignmentSubmissions maps dueDateReminderViewStates correctly`() { + val course = + Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + val checkpoint1 = Checkpoint( + tag = "reply_to_topic", + dueAt = Calendar.getInstance() + .apply { add(Calendar.DAY_OF_MONTH, 1) }.time.toApiString() + ) + val checkpoint2 = Checkpoint( + tag = "reply_to_entry", + dueAt = Calendar.getInstance() + .apply { add(Calendar.DAY_OF_MONTH, 2) }.time.toApiString() + ) + + val subSubmission1 = SubAssignmentSubmission( + subAssignmentTag = "reply_to_topic", + + ) + val subSubmission2 = SubAssignmentSubmission(subAssignmentTag = "reply_to_entry") + + + val assignment = Assignment( + checkpoints = listOf(checkpoint1, checkpoint2), + submission = Submission( + subAssignmentSubmissions = arrayListOf(subSubmission1, subSubmission2) + ) + ) + coEvery { + assignmentDetailsRepository.getAssignment( + any(), + any(), + any(), + any() + ) + } returns assignment + + val viewModel = getViewModel() + + assertEquals(2, viewModel.dueDateReminderViewStates.size) + assertEquals("reply_to_topic", viewModel.dueDateReminderViewStates[0].tag) + assertEquals("reply_to_entry", viewModel.dueDateReminderViewStates[1].tag) + + assertTrue(viewModel.dueDateReminderViewStates[0].reminders.isEmpty()) + assertTrue(viewModel.dueDateReminderViewStates[1].reminders.isEmpty()) + } } diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/AssignmentFacadeTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/AssignmentFacadeTest.kt index cf3de7f4c8..79d099d9a6 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/AssignmentFacadeTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/AssignmentFacadeTest.kt @@ -21,6 +21,7 @@ import androidx.room.withTransaction import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.AssignmentGroup import com.instructure.canvasapi2.models.AssignmentScoreStatistics +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.LockInfo import com.instructure.canvasapi2.models.PlannableType @@ -33,6 +34,7 @@ import com.instructure.pandautils.room.offline.daos.AssignmentDao import com.instructure.pandautils.room.offline.daos.AssignmentGroupDao import com.instructure.pandautils.room.offline.daos.AssignmentRubricCriterionDao import com.instructure.pandautils.room.offline.daos.AssignmentScoreStatisticsDao +import com.instructure.pandautils.room.offline.daos.CheckpointDao import com.instructure.pandautils.room.offline.daos.PlannerOverrideDao import com.instructure.pandautils.room.offline.daos.RubricCriterionDao import com.instructure.pandautils.room.offline.daos.RubricCriterionRatingDao @@ -41,6 +43,7 @@ import com.instructure.pandautils.room.offline.entities.AssignmentEntity import com.instructure.pandautils.room.offline.entities.AssignmentGroupEntity import com.instructure.pandautils.room.offline.entities.AssignmentRubricCriterionEntity import com.instructure.pandautils.room.offline.entities.AssignmentScoreStatisticsEntity +import com.instructure.pandautils.room.offline.entities.CheckpointEntity import com.instructure.pandautils.room.offline.entities.PlannerOverrideEntity import com.instructure.pandautils.room.offline.entities.RubricCriterionEntity import com.instructure.pandautils.room.offline.entities.RubricSettingsEntity @@ -73,6 +76,7 @@ class AssignmentFacadeTest { private val lockInfoFacade: LockInfoFacade = mockk(relaxed = true) private val rubricCriterionRatingDao: RubricCriterionRatingDao = mockk(relaxed = true) private val assignmentRubricCriterionDao: AssignmentRubricCriterionDao = mockk(relaxed = true) + private val checkpointDao: CheckpointDao = mockk(relaxed = true) private val offlineDatabase: OfflineDatabase = mockk(relaxed = true) private val facade = AssignmentFacade( @@ -87,6 +91,7 @@ class AssignmentFacadeTest { lockInfoFacade, rubricCriterionRatingDao, assignmentRubricCriterionDao, + checkpointDao, offlineDatabase ) @@ -118,6 +123,7 @@ class AssignmentFacadeTest { val scoreStatistics = AssignmentScoreStatistics(0.0, 0.0, 0.0) val rubricCriterions = listOf(RubricCriterion()) val lockInfo = LockInfo() + val checkpoints = listOf(Checkpoint(name = "Checkpoint 1", tag = "checkpoint_1")) val assignments = listOf( Assignment( rubricSettings = rubricSettings, @@ -128,6 +134,7 @@ class AssignmentFacadeTest { rubric = rubricCriterions, lockInfo = lockInfo, courseId = 1, + checkpoints = checkpoints ) ) val assignmentGroups = listOf(AssignmentGroup(assignments = assignments)) @@ -141,6 +148,7 @@ class AssignmentFacadeTest { coEvery { assignmentScoreStatisticsDao.insert(any()) } just Runs coEvery { rubricCriterionDao.insert(any()) } just Runs coEvery { lockInfoFacade.insertLockInfoForAssignment(any(), any()) } just Runs + coEvery { checkpointDao.insertAll(any()) } just Runs facade.insertAssignmentGroups(assignmentGroups, 1L) @@ -157,6 +165,7 @@ class AssignmentFacadeTest { coVerify { assignmentRubricCriterionDao.insert(AssignmentRubricCriterionEntity(assignment.id, it.id.orEmpty())) } } coVerify { lockInfoFacade.insertLockInfoForAssignment(lockInfo, assignment.id) } + coVerify { checkpointDao.insertAll(checkpoints.map { CheckpointEntity(it, assignment.id) }) } coVerify { assignmentDao.insertOrUpdate( AssignmentEntity( @@ -181,6 +190,7 @@ class AssignmentFacadeTest { val scoreStatistics = AssignmentScoreStatistics(0.0, 0.0, 0.0) val rubricCriterions = listOf(RubricCriterion()) val lockInfo = LockInfo() + val checkpoints = listOf(Checkpoint(name = "Checkpoint 1", tag = "checkpoint_1")) val assignment = Assignment( rubricSettings = rubricSettings, submission = submission, @@ -190,6 +200,7 @@ class AssignmentFacadeTest { rubric = rubricCriterions, lockInfo = lockInfo, courseId = 1, + checkpoints = checkpoints ) coEvery { assignmentDao.insert(any()) } just Runs @@ -200,6 +211,7 @@ class AssignmentFacadeTest { coEvery { assignmentScoreStatisticsDao.insert(any()) } just Runs coEvery { rubricCriterionDao.insert(any()) } just Runs coEvery { lockInfoFacade.insertLockInfoForAssignment(any(), any()) } just Runs + coEvery { checkpointDao.insertAll(any()) } just Runs facade.insertAssignment(assignment) @@ -219,6 +231,7 @@ class AssignmentFacadeTest { coVerify { rubricCriterionDao.insert(RubricCriterionEntity(it, assignment.id)) } } coVerify { lockInfoFacade.insertLockInfoForAssignment(lockInfo, assignment.id) } + coVerify { checkpointDao.insertAll(checkpoints.map { CheckpointEntity(it, assignment.id) }) } coVerify { assignmentDao.insertOrUpdate( AssignmentEntity( diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/SubmissionFacadeTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/SubmissionFacadeTest.kt index de305395b4..e853245644 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/SubmissionFacadeTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/SubmissionFacadeTest.kt @@ -26,6 +26,7 @@ import com.instructure.pandautils.room.offline.daos.AuthorDao import com.instructure.pandautils.room.offline.daos.GroupDao import com.instructure.pandautils.room.offline.daos.MediaCommentDao import com.instructure.pandautils.room.offline.daos.RubricCriterionAssessmentDao +import com.instructure.pandautils.room.offline.daos.SubAssignmentSubmissionDao import com.instructure.pandautils.room.offline.daos.SubmissionCommentDao import com.instructure.pandautils.room.offline.daos.SubmissionDao import com.instructure.pandautils.room.offline.daos.UserDao @@ -33,8 +34,10 @@ import com.instructure.pandautils.room.offline.entities.GroupEntity import com.instructure.pandautils.room.offline.entities.MediaCommentEntity import com.instructure.pandautils.room.offline.entities.SubmissionEntity import com.instructure.pandautils.room.offline.entities.UserEntity +import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.just import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Assert @@ -50,10 +53,11 @@ class SubmissionFacadeTest { private val attachmentDao: AttachmentDao = mockk(relaxed = true) private val authorDao: AuthorDao = mockk(relaxed = true) private val rubricCriterionAssessmentDao: RubricCriterionAssessmentDao = mockk(relaxed = true) + private val subAssignmentSubmissionDao: SubAssignmentSubmissionDao = mockk(relaxed = true) private val facade = SubmissionFacade( submissionDao, groupDao, mediaCommentDao, userDao, - submissionCommentDao, attachmentDao, authorDao, rubricCriterionAssessmentDao + submissionCommentDao, attachmentDao, authorDao, rubricCriterionAssessmentDao, subAssignmentSubmissionDao ) @Test @@ -73,6 +77,7 @@ class SubmissionFacadeTest { ) coEvery { submissionDao.insert(any()) } returns 1L + coEvery { subAssignmentSubmissionDao.insertAll(any()) } just Runs facade.insertSubmission(submission) @@ -80,6 +85,7 @@ class SubmissionFacadeTest { coVerify { mediaCommentDao.insert(MediaCommentEntity(mediaComment, 1L, 0)) } coVerify { userDao.insertOrUpdate(UserEntity(user)) } coVerify { submissionDao.insertOrUpdate(SubmissionEntity(submission, group.id, mediaComment.mediaId)) } + coVerify { subAssignmentSubmissionDao.insertAll(emptyList()) } } @Test @@ -95,6 +101,7 @@ class SubmissionFacadeTest { coEvery { mediaCommentDao.findById(any()) } returns MediaCommentEntity(mediaComment, 1L, 0) coEvery { userDao.findById(any()) } returns UserEntity(user) coEvery { submissionDao.findById(any()) } returns submissionHistory.map { SubmissionEntity(it, group.id, mediaComment.mediaId) } + coEvery { subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(any(), any()) } returns emptyList() val result = facade.getSubmissionById(submissionId)!! @@ -134,6 +141,7 @@ class SubmissionFacadeTest { ) } coEvery { submissionDao.findById(submissionId) } returns submissionHistory.map { SubmissionEntity(it, group.id, mediaComment.mediaId) } + coEvery { subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(any(), any()) } returns emptyList() val result = facade.findByAssignmentIds(listOf(assignmentId))