From 54168aefd8d5f755225ea271846f34d98e811a13 Mon Sep 17 00:00:00 2001 From: "andras.maczak" Date: Wed, 17 Sep 2025 15:03:46 +0200 Subject: [PATCH 01/23] student assignment details screen changes --- .../src/main/java/GlobalDependencies.kt | 2 + .../canvasapi2/apis/AssignmentAPI.kt | 2 +- libs/pandares/src/main/res/values/strings.xml | 2 + libs/pandautils/build.gradle | 1 + .../12.json | 12 +- .../13.json | 722 ++++++++++++++++++ .../details/AssignmentDetailsFragment.kt | 22 +- .../details/AssignmentDetailsViewModel.kt | 46 +- .../composables/DueDateReminderLayout.kt | 72 ++ .../features/reminder/ReminderManager.kt | 11 +- .../features/reminder/ReminderRepository.kt | 6 +- .../features/reminder/ReminderViewState.kt | 2 + .../reminder/composables/ReminderView.kt | 6 +- .../room/appdatabase/AppDatabase.kt | 2 +- .../room/appdatabase/AppDatabaseMigrations.kt | 6 +- .../appdatabase/entities/ReminderEntity.kt | 3 +- .../layout/fragment_assignment_details.xml | 8 +- 17 files changed, 902 insertions(+), 23 deletions(-) create mode 100644 libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/13.json create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DueDateReminderLayout.kt diff --git a/apps/buildSrc/src/main/java/GlobalDependencies.kt b/apps/buildSrc/src/main/java/GlobalDependencies.kt index affae9257d..5dba5cf184 100644 --- a/apps/buildSrc/src/main/java/GlobalDependencies.kt +++ b/apps/buildSrc/src/main/java/GlobalDependencies.kt @@ -44,6 +44,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 { @@ -129,6 +130,7 @@ 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" + const val COMPOSE_LIVEDATA = "androidx.compose.runtime:runtime-livedata:${Versions.LIVEDATA}" /* Media and content handling */ const val PSPDFKIT = "com.pspdfkit:pspdfkit:${Versions.PSPDFKIT}" 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 f0d3e55a8f..5cf4298665 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, diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index 58d77ad6b5..75b0d4a342 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -2151,5 +2151,7 @@ Write days late Reply to topic Additional replies (%d) + Reply to topic due + Additional replies (%d) due Discussion Checkpoints diff --git a/libs/pandautils/build.gradle b/libs/pandautils/build.gradle index 9cce7f814b..f1277eae5b 100644 --- a/libs/pandautils/build.gradle +++ b/libs/pandautils/build.gradle @@ -260,4 +260,5 @@ dependencies { implementation Libs.LOTTIE_COMPOSE implementation Libs.DISK_LRU_CACHE + implementation Libs.COMPOSE_LIVEDATA } diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/12.json b/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/12.json index 32fc5f831d..eb2ff1c69c 100644 --- a/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/12.json +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/12.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 12, - "identityHash": "51a92e7d32ab68ff24af5213c6d6c162", + "identityHash": "d7eba14162e2c9edf9afca9a2e1b860e", "entities": [ { "tableName": "AttachmentEntity", @@ -502,7 +502,7 @@ }, { "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)", + "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 NOT NULL)", "fields": [ { "fieldPath": "id", @@ -545,6 +545,12 @@ "columnName": "time", "affinity": "INTEGER", "notNull": true + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": true } ], "primaryKey": { @@ -710,7 +716,7 @@ "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, '51a92e7d32ab68ff24af5213c6d6c162')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd7eba14162e2c9edf9afca9a2e1b860e')" ] } } \ No newline at end of file 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/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 0d9d3fbd64..89fa9aef29 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 @@ -154,6 +155,21 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo } ) } + binding?.dueComposeView?.setContent { + val states = viewModel.dueDatesViewState + DueDateReminderLayout( + states, + onAddClick = { checkAlarmPermission() }, + onRemoveClick = { reminderId -> + viewModel.showDeleteReminderConfirmationDialog( + requireContext(), + reminderId, + assignmentDetailsBehaviour.dialogColor + ) + } + ) + } + return binding?.root } @@ -365,14 +381,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( @@ -383,7 +399,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/AssignmentDetailsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt index ff70bf6b11..509773d9d4 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 @@ -67,10 +68,12 @@ import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.HtmlContentFormatter 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 import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -134,6 +137,10 @@ class AssignmentDetailsViewModel @Inject constructor( private val _reminderViewState = MutableStateFlow(ReminderViewState()) val reminderViewState = _reminderViewState.asStateFlow() + private val _dueDatesViewState = mutableStateListOf() + val dueDatesViewState: List + get() = _dueDatesViewState + var checkingReminderPermission = false var checkingNotificationPermission = false @@ -159,9 +166,15 @@ class AssignmentDetailsViewModel @Inject constructor( dueDate = assignment?.dueDate ) } _data.value?.notifyPropertyChanged(BR.reminders) + + } } + private fun updateDueDatesViewState(reminderEntities: List) { + + } + fun getVideoUri(fragment: FragmentActivity): Uri? = submissionHandler.getVideoUri(fragment) override fun onCleared() { @@ -227,6 +240,31 @@ class AssignmentDetailsViewModel @Inject constructor( _reminderViewState.update { it.copy( dueDate = if (assignment?.submission?.excused.orDefault()) null else assignment?.dueDate ) } + + if (assignment?.checkpoints?.isNotEmpty() == true) { + _dueDatesViewState.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) + } + _dueDatesViewState.add( + ReminderViewState( + dueLabel = dueLabel, + themeColor = Color.Red, + dueDate = checkpoint.dueDate, + tag = checkpoint.tag, + ) + ) + } + } _data.postValue(getViewData(assignmentResult, hasDraft)) _state.postValue(ViewState.Success) } catch (ex: Exception) { @@ -629,7 +667,7 @@ class AssignmentDetailsViewModel @Inject constructor( _reminderViewState.update { it.copy(themeColor = Color(color)) } } - fun showCreateReminderDialog(context: Context, @ColorInt color: Int) { + fun showCreateReminderDialog(context: Context, @ColorInt color: Int, tag: String? = null) { assignment?.let { assignment -> viewModelScope.launch { when { @@ -639,7 +677,8 @@ class AssignmentDetailsViewModel @Inject constructor( assignment.id, assignment.name.orEmpty(), assignment.htmlUrl.orEmpty(), - assignment.dueDate + assignment.dueDate, + tag ) assignment.dueDate?.before(Date()).orDefault() -> reminderManager.showCustomReminderDialog( context, @@ -647,7 +686,8 @@ class AssignmentDetailsViewModel @Inject constructor( assignment.id, assignment.name.orEmpty(), assignment.htmlUrl.orEmpty(), - assignment.dueDate + assignment.dueDate, + tag ) else -> reminderManager.showBeforeDueDateReminderDialog( context, 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..78f62f2a0f --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DueDateReminderLayout.kt @@ -0,0 +1,72 @@ +/* + * 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.res.colorResource +import androidx.compose.ui.res.stringResource +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.padding(horizontal = 16.dp)) { + for (reminderViewState in reminderViewStates) { + DueDateBlock(reminderViewState) + ReminderView( + viewState = reminderViewState, + onAddClick = onAddClick, + onRemoveClick = onRemoveClick + ) + CanvasDivider() + } + } +} + +@Composable +private fun DueDateBlock( + reminderViewState: ReminderViewState +) { + Text( + modifier = Modifier.padding(top = 24.dp), + 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), + text = "${reminderViewState.dueDate?.toFormattedString()}", + 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..60ce6804cd 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 @@ -115,10 +115,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 +186,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 +222,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..7c30f758e8 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,7 +51,7 @@ import com.instructure.pandautils.utils.toFormattedString @Composable fun ReminderView( viewState: ReminderViewState, - onAddClick: () -> Unit, + onAddClick: (String?) -> Unit, onRemoveClick: (Long) -> Unit, ) { CanvasTheme { @@ -71,9 +71,9 @@ 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/res/layout/fragment_assignment_details.xml b/libs/pandautils/src/main/res/layout/fragment_assignment_details.xml index e7b42ce6cf..c84cfd61d1 100644 --- a/libs/pandautils/src/main/res/layout/fragment_assignment_details.xml +++ b/libs/pandautils/src/main/res/layout/fragment_assignment_details.xml @@ -279,6 +279,12 @@ android:visibility="@{viewModel.data.dueDate.empty ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toBottomOf="@id/lockedMessageTextView" /> + + + app:layout_constraintTop_toBottomOf="@id/dueComposeView" /> Date: Thu, 18 Sep 2025 11:17:20 +0200 Subject: [PATCH 02/23] updated reminder logic, removed unnecessary code --- .../details/AssignmentDetailsFragment.kt | 15 +--- .../details/AssignmentDetailsViewModel.kt | 76 ++++++++++++------- .../composables/DueDateReminderLayout.kt | 12 ++- .../features/reminder/ReminderManager.kt | 7 +- .../reminder/composables/ReminderView.kt | 4 +- .../layout/fragment_assignment_details.xml | 50 +----------- 6 files changed, 70 insertions(+), 94 deletions(-) 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 89fa9aef29..782db62b7f 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 @@ -145,21 +145,12 @@ 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() }, - onRemoveClick = { reminderId -> - viewModel.showDeleteReminderConfirmationDialog(requireContext(), reminderId, assignmentDetailsBehaviour.dialogColor) - } - ) - } + binding?.dueComposeView?.setContent { - val states = viewModel.dueDatesViewState + val states = viewModel.dueDateReminderViewStates DueDateReminderLayout( states, - onAddClick = { checkAlarmPermission() }, + onAddClick = { tag -> checkAlarmPermission(tag) }, onRemoveClick = { reminderId -> viewModel.showDeleteReminderConfirmationDialog( requireContext(), 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 509773d9d4..d140adf63f 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 @@ -73,7 +73,6 @@ import com.instructure.pandautils.utils.toFormattedString import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -134,12 +133,11 @@ class AssignmentDetailsViewModel @Inject constructor( private var selectedSubmission: Submission? = null - private val _reminderViewState = MutableStateFlow(ReminderViewState()) - val reminderViewState = _reminderViewState.asStateFlow() - - private val _dueDatesViewState = mutableStateListOf() - val dueDatesViewState: List - get() = _dueDatesViewState + 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 @@ -161,18 +159,29 @@ class AssignmentDetailsViewModel @Inject constructor( 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) @@ -237,12 +246,9 @@ 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) { - _dueDatesViewState.clear() + _dueDateReminderViewStates.clear() assignment?.orderedCheckpoints?.forEach { checkpoint -> val dueLabel = when (checkpoint.tag) { Const.REPLY_TO_TOPIC -> application.getString(R.string.reply_to_topic_due) @@ -255,15 +261,28 @@ class AssignmentDetailsViewModel @Inject constructor( else -> application.getString(R.string.dueLabel) } - _dueDatesViewState.add( + val subAssignment = assignment?.submission?.subAssignmentSubmissions?.firstOrNull { it.subAssignmentTag == checkpoint.tag } + _dueDateReminderViewStates.add( ReminderViewState( dueLabel = dueLabel, - themeColor = Color.Red, - dueDate = checkpoint.dueDate, + 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) @@ -664,29 +683,33 @@ 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, 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( @@ -695,8 +718,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 index 78f62f2a0f..3dda6cde12 100644 --- 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 @@ -24,6 +24,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier 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 @@ -39,7 +41,7 @@ fun DueDateReminderLayout( onRemoveClick: (Long) -> Unit, modifier: Modifier = Modifier ) { - Column(modifier = modifier.padding(horizontal = 16.dp)) { + Column { for (reminderViewState in reminderViewStates) { DueDateBlock(reminderViewState) ReminderView( @@ -57,15 +59,17 @@ private fun DueDateBlock( reminderViewState: ReminderViewState ) { Text( - modifier = Modifier.padding(top = 24.dp), + modifier = Modifier + .padding(top = 24.dp, start = 16.dp, end = 16.dp) + .semantics { heading() }, 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), - text = "${reminderViewState.dueDate?.toFormattedString()}", + modifier = Modifier.padding(bottom = 14.dp, start = 16.dp, end = 16.dp), + text = "${reminderViewState.dueDate?.toFormattedString() ?: stringResource(R.string.toDoNoDueDate)}", color = colorResource(id = R.color.textDarkest), fontSize = 16.sp ) 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 60ce6804cd..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), 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 7c30f758e8..f52f2e30cd 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 @@ -71,7 +71,9 @@ fun ReminderView( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .padding(vertical = 12.dp) - .clickable { onAddClick(viewState.tag) } + .clickable { + onAddClick(viewState.tag) + } ) { IconButton(onClick = { onAddClick(viewState.tag) }) { Icon( 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 c84cfd61d1..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/dueComposeView" /> Date: Fri, 19 Sep 2025 15:09:34 +0200 Subject: [PATCH 03/23] unit tests added --- .../details/AssignmentDetailsViewData.kt | 3 +- .../details/AssignmentDetailsViewModel.kt | 6 +- .../composables/DueDateReminderLayout.kt | 13 +- .../details/AssignmentDetailsViewModelTest.kt | 191 +++++++++++++++++- 4 files changed, 197 insertions(+), 16 deletions(-) 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 d140adf63f..40d3d3639d 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 @@ -158,9 +158,6 @@ class AssignmentDetailsViewModel @Inject constructor( loadData() reminderManager.observeRemindersLiveData(apiPrefs.user?.id.orDefault(), assignmentId) { reminderEntities -> - _data.value?.reminders = mapReminders(reminderEntities) - _data.value?.notifyPropertyChanged(BR.reminders) - this.reminderEntities = reminderEntities updateDueDatesViewState(reminderEntities) } @@ -533,8 +530,7 @@ class AssignmentDetailsViewModel @Inject constructor( discussionHeaderViewData = discussionHeaderViewData, quizDetails = quizViewViewData, attemptsViewData = attemptsViewData, - hasDraft = hasDraft, - reminders = _data.value?.reminders.orEmpty(), + hasDraft = hasDraft ) } 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 index 3dda6cde12..13d481c8c4 100644 --- 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 @@ -22,6 +22,7 @@ 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 @@ -41,9 +42,9 @@ fun DueDateReminderLayout( onRemoveClick: (Long) -> Unit, modifier: Modifier = Modifier ) { - Column { - for (reminderViewState in reminderViewStates) { - DueDateBlock(reminderViewState) + Column(modifier = modifier) { + reminderViewStates.forEachIndexed { index, reminderViewState -> + DueDateBlock(reminderViewState, index) ReminderView( viewState = reminderViewState, onAddClick = onAddClick, @@ -56,12 +57,14 @@ fun DueDateReminderLayout( @Composable private fun DueDateBlock( - reminderViewState: ReminderViewState + reminderViewState: ReminderViewState, + position: Int ) { Text( modifier = Modifier .padding(top = 24.dp, start = 16.dp, end = 16.dp) - .semantics { heading() }, + .semantics { heading() } + .testTag("dueDateHeaderText-$position"), text = reminderViewState.dueLabel ?: stringResource(id = R.string.dueLabel), color = colorResource(id = R.color.textDark), fontSize = 14.sp diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt index 657c9569e8..8cfeb56864 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt @@ -30,11 +30,13 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import com.instructure.canvasapi2.CustomGradeStatusesQuery import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.Enrollment import com.instructure.canvasapi2.models.LockInfo import com.instructure.canvasapi2.models.Quiz +import com.instructure.canvasapi2.models.SubAssignmentSubmission import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.Analytics @@ -797,8 +799,71 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel(realReminderManager) assertEquals( - reminderEntities.map { ReminderViewData(it.id, it.text) }, - viewModel.data.value?.reminders?.map { it.data } + reminderEntities.map { it.id }, + viewModel.dueDateReminderViewStates[0].reminders.map { it.id } + ) + } + + @Test + fun `Reminders map correctly for discussion checkpoints`() { + val reminderEntities = listOf( + ReminderEntity(1, 1, 1, "htmlUrl1", "Assignment 1", "1 day", 1000, "reply_to_topic"), + ReminderEntity(2, 1, 1, "htmlUrl2", "Assignment 2", "2 days", 2000, "reply_to_topic"), + ReminderEntity(3, 1, 1, "htmlUrl3", "Assignment 3", "3 days", 3000, "reply_to_entry") + ) + val dateTimePicker: DateTimePicker = mockk(relaxed = true) + val reminderRepository: ReminderRepository = mockk(relaxed = true) + val realReminderManager = ReminderManager(dateTimePicker, reminderRepository, analytics) + + every { reminderRepository.findByAssignmentIdLiveData(any(), any()) } returns MutableLiveData(reminderEntities) + 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( + 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 } ) } @@ -822,11 +887,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 @@ -1008,4 +1142,53 @@ class AssignmentDetailsViewModelTest { ) } } + + @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()) + } } From 06844493072ef570f89d249259ad95da7ef1b0ad Mon Sep 17 00:00:00 2001 From: "andras.maczak" Date: Tue, 23 Sep 2025 14:52:04 +0200 Subject: [PATCH 04/23] added interaction tests --- .../AssignmentDetailsInteractionTest.kt | 70 +++++++++++++++++- .../ParentCalendarInteractionTest.kt | 2 +- .../parentapp/utils/ParentComposeTest.kt | 3 + .../instructure/parentapp/utils/ParentTest.kt | 3 - .../student/ui/e2e/FilesE2ETest.kt | 12 +--- .../student/ui/e2e/GradesE2ETest.kt | 4 +- .../student/ui/e2e/ModulesE2ETest.kt | 4 +- .../student/ui/e2e/ShareExtensionE2ETest.kt | 12 +--- .../ui/e2e/k5/ImportantDatesE2ETest.kt | 7 +- .../student/ui/e2e/k5/ScheduleE2ETest.kt | 5 +- .../ui/e2e/offline/OfflineGradesE2ETest.kt | 4 +- .../ui/e2e/offline/OfflineModulesE2ETest.kt | 4 +- .../AssignmentDetailsInteractionTest.kt | 72 ++++++++++++++++++- .../ui/interaction/ModuleInteractionTest.kt | 4 +- .../NotificationInteractionTest.kt | 4 +- .../PickerSubmissionUploadInteractionTest.kt | 11 +-- .../ui/interaction/ScheduleInteractionTest.kt | 4 +- .../StudentCalendarInteractionTest.kt | 2 +- .../SubmissionDetailsInteractionTest.kt | 12 +--- .../ui/interaction/TodoInteractionTest.kt | 4 +- .../ui/pages/StudentAssignmentDetailsPage.kt | 3 +- .../student/ui/utils/StudentComposeTest.kt | 10 +++ .../student/ui/utils/StudentTest.kt | 2 - .../interaction/GradesInteractionTest.kt | 2 +- .../interaction/SmartSearchInteractionTest.kt | 2 +- .../common/pages/AssignmentDetailsPage.kt | 17 +++-- .../canvas/espresso/mockCanvas/MockCanvas.kt | 50 ++++++++----- .../endpoints/AssignmentEndpoints.kt | 3 +- .../canvasapi2/apis/AssignmentAPI.kt | 2 +- .../composables/DueDateReminderLayout.kt | 9 ++- .../reminder/composables/ReminderView.kt | 3 +- 31 files changed, 242 insertions(+), 104 deletions(-) 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 e6553daca7..75e56d1841 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() { reminderPage.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 422e6385be..95ea995d9b 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 d13e612fb6..d4bc9337c9 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.ReminderPage 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.AddStudentBottomPage import com.instructure.parentapp.ui.pages.AlertsPage @@ -80,6 +82,7 @@ abstract class ParentComposeTest : ParentTest() { protected val calendarFilterPage = CalendarFilterPage(composeTestRule) protected val reminderPage = ReminderPage(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 40dbfce721..b9b11b382b 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.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/FilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt index 04fb9f8848..84a4a92c02 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt @@ -18,7 +18,6 @@ package com.instructure.student.ui.e2e 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 @@ -27,7 +26,6 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage import com.instructure.canvasapi2.managers.DiscussionManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.weave.awaitApiResponse @@ -43,29 +41,23 @@ 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.ViewUtils import com.instructure.student.ui.utils.seedData import com.instructure.student.ui.utils.tokenLogin import com.instructure.student.ui.utils.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/GradesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt index ec06b1feea..f933055adc 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/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.seedData import com.instructure.student.ui.utils.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/ModulesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt index c03e3b3f0a..9e9fec0a56 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/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.seedData import com.instructure.student.ui.utils.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/ShareExtensionE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt index 3f03cf856d..8e00b69279 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt @@ -19,39 +19,31 @@ package com.instructure.student.ui.e2e 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.E2E -import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType 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.ViewUtils import com.instructure.student.ui.utils.seedData import com.instructure.student.ui.utils.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) } - @E2E @Test fun shareExtensionE2ETest() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt index 5c7be151e0..3388b77e4d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt @@ -31,16 +31,17 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.ui.pages.ElementaryDashboardPage -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.seedDataForK5 import com.instructure.student.ui.utils.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.text.SimpleDateFormat -import java.util.* +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/k5/ScheduleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt index 04dc448f9a..80178903b9 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt @@ -25,7 +25,6 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvasapi2.type.AssetString import com.instructure.canvasapi2.utils.toApiString import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType @@ -34,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.ElementaryDashboardPage -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.seedDataForK5 import com.instructure.student.ui.utils.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest @@ -47,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/offline/OfflineGradesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineGradesE2ETest.kt index 925461cbda..189df89ffa 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineGradesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineGradesE2ETest.kt @@ -38,7 +38,7 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.getDateInCanvasCalendarFormat import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.seedData import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @@ -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/offline/OfflineModulesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineModulesE2ETest.kt index 1e7a7968db..5edc4d09f5 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineModulesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineModulesE2ETest.kt @@ -39,7 +39,7 @@ import com.instructure.dataseeding.util.iso8601 import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.seedData import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @@ -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 3eee878026..5089698340 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() { reminderPage.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 9310441190..f343e9a9b4 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.WebViewTextCheck -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.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 71c6db7740..b2b6e9d6d7 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.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 05b8c5b8c0..c5e58f4512 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.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -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.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 da968141ff..8448a01f6b 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.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.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 a99ffde9bc..d07e18ef83 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 926865ae97..6f05dfc9e4 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.Espresso import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.web.webdriver.Locator @@ -28,7 +27,6 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -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.RubricCriterion import com.instructure.canvasapi2.models.RubricCriterionRating import com.instructure.canvasapi2.models.SubmissionComment import com.instructure.student.ui.pages.WebViewTextCheck -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.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 d7cf229736..d1904e41fb 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.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/StudentAssignmentDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/StudentAssignmentDetailsPage.kt index 48d5c0c588..337a5d8aef 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/StudentAssignmentDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/StudentAssignmentDetailsPage.kt @@ -16,6 +16,7 @@ package com.instructure.student.ui.pages 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 4350c4c16a..38cf602d39 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.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 e10448467b..11bf48442e 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 @@ -91,7 +91,6 @@ import com.instructure.student.ui.pages.ResourcesPage import com.instructure.student.ui.pages.SchedulePage import com.instructure.student.ui.pages.ShareExtensionStatusPage import com.instructure.student.ui.pages.ShareExtensionTargetPage -import com.instructure.student.ui.pages.StudentAssignmentDetailsPage import com.instructure.student.ui.pages.SubmissionDetailsPage import com.instructure.student.ui.pages.SyllabusPage import com.instructure.student.ui.pages.TextSubmissionUploadPage @@ -121,7 +120,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 cd6efb603d..fe6ed00cae 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 6c801164a8..bb80601ded 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 5aa1f845fe..74dedcc8f6 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,12 @@ package com.instructure.canvas.espresso.common.pages import android.view.View import android.widget.ScrollView +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag import androidx.test.espresso.AmbiguousViewMatcherException import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onData @@ -68,10 +74,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 +91,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) { @@ -250,8 +255,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").assertIsDisplayed() } 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 a194421cf8..69aab10b78 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 @@ -1104,29 +1105,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 114413018a..e69c8e53b0 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 @@ -219,5 +219,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 fb50ff4cc3..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 @@ -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/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 index 13d481c8c4..b7c7705ac0 100644 --- 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 @@ -48,7 +48,8 @@ fun DueDateReminderLayout( ReminderView( viewState = reminderViewState, onAddClick = onAddClick, - onRemoveClick = onRemoveClick + onRemoveClick = onRemoveClick, + modifier = Modifier.testTag("reminderView-$index") ) CanvasDivider() } @@ -71,8 +72,10 @@ private fun DueDateBlock( ) Spacer(modifier = Modifier.height(2.dp)) Text( - modifier = Modifier.padding(bottom = 14.dp, start = 16.dp, end = 16.dp), - text = "${reminderViewState.dueDate?.toFormattedString() ?: stringResource(R.string.toDoNoDueDate)}", + 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 ) 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 f52f2e30cd..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, + modifier: Modifier = Modifier, onAddClick: (String?) -> Unit, onRemoveClick: (Long) -> Unit, ) { CanvasTheme { Column( - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding(vertical = 24.dp, horizontal = 16.dp) ) { From 4c4b9ff0360559051f092403cfead927a86272f9 Mon Sep 17 00:00:00 2001 From: "andras.maczak" Date: Tue, 23 Sep 2025 21:20:43 +0200 Subject: [PATCH 05/23] fixed tests --- .../instructure/parentapp/ui/e2e/AssignmentReminderE2ETest.kt | 2 +- .../canvas/espresso/common/pages/AssignmentDetailsPage.kt | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AssignmentReminderE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AssignmentReminderE2ETest.kt index 25fb89b8f3..0b5e89a904 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AssignmentReminderE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/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.") reminderPage.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'.") reminderPage.clickCustomReminderOption() reminderPage.selectDate(reminderDateOneDay) 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 74dedcc8f6..3ed2338145 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,11 +18,9 @@ package com.instructure.canvas.espresso.common.pages import android.view.View import android.widget.ScrollView -import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onNodeWithTag import androidx.test.espresso.AmbiguousViewMatcherException import androidx.test.espresso.Espresso @@ -256,7 +254,7 @@ open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteracti } fun assertReminderViewDisplayed(position: Int = 0) { - composeTestRule.onNodeWithTag("reminderView-$position").assertIsDisplayed() + composeTestRule.onNodeWithTag("reminderView-$position").assertExists() } fun assertNoDescriptionViewDisplayed() { From a14fbd9211ac898711ecd4e1186a019260c8e7ee Mon Sep 17 00:00:00 2001 From: "andras.maczak" Date: Wed, 24 Sep 2025 13:46:54 +0200 Subject: [PATCH 06/23] fixed tests --- .../student/ui/interaction/SubmissionDetailsInteractionTest.kt | 2 ++ .../main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) 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 6f05dfc9e4..970305637a 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 @@ -68,6 +68,7 @@ class SubmissionDetailsInteractionTest : StudentComposeTest() { // Should be able to add a comment on a submission @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) + @Stub fun testComments_addCommentToSingleAttemptSubmission() { val data = getToCourse() @@ -90,6 +91,7 @@ class SubmissionDetailsInteractionTest : StudentComposeTest() { @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) + @Stub fun testComments_addCommentToMultipleAttemptSubmission() { val data = getToCourse() val assignment = data.addAssignment( diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt index aeeb44a087..6ee3e98c98 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt @@ -32,7 +32,7 @@ class ScreenshotTestRule : TestRule { // Run all test methods tryCount times. Take screenshots on failure. // A method rule would allow targeting specific (method.getAnnotation(Retry.class)) - private val tryCount = 5 + private val tryCount = 1 override fun apply(base: Statement, description: Description): Statement { return object : Statement() { From 24c8c602e064fe5b048772435994a763a14c6bd9 Mon Sep 17 00:00:00 2001 From: "andras.maczak" Date: Wed, 24 Sep 2025 13:47:46 +0200 Subject: [PATCH 07/23] tryCount change reverted --- .../main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt index 6ee3e98c98..aeeb44a087 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt @@ -32,7 +32,7 @@ class ScreenshotTestRule : TestRule { // Run all test methods tryCount times. Take screenshots on failure. // A method rule would allow targeting specific (method.getAnnotation(Retry.class)) - private val tryCount = 1 + private val tryCount = 5 override fun apply(base: Statement, description: Description): Statement { return object : Statement() { From 3446eb482bb58b5fc5bf637ec5f955b1a2d8c065 Mon Sep 17 00:00:00 2001 From: "kristof.nemere" Date: Tue, 7 Oct 2025 15:35:03 +0200 Subject: [PATCH 08/23] Offline DCP --- .../6.json | 5826 +++++++++++++++++ .../room/offline/daos/CheckpointDaoTest.kt | 231 + .../daos/SubAssignmentSubmissionDaoTest.kt | 342 + .../pandautils/di/OfflineModule.kt | 20 +- .../room/offline/OfflineDatabase.kt | 14 +- .../room/offline/OfflineDatabaseMigrations.kt | 35 + .../room/offline/daos/CheckpointDao.kt | 40 + .../daos/SubAssignmentSubmissionDao.kt | 40 + .../room/offline/entities/AssignmentEntity.kt | 6 +- .../room/offline/entities/CheckpointEntity.kt | 68 + .../entities/SubAssignmentSubmissionEntity.kt | 84 + .../room/offline/entities/SubmissionEntity.kt | 14 +- .../room/offline/facade/AssignmentFacade.kt | 9 +- .../room/offline/facade/SubmissionFacade.kt | 11 +- 14 files changed, 6727 insertions(+), 13 deletions(-) create mode 100644 libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/6.json create mode 100644 libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CheckpointDaoTest.kt create mode 100644 libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDaoTest.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CheckpointDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CheckpointEntity.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubAssignmentSubmissionEntity.kt 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..fc3d9231c7 --- /dev/null +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/6.json @@ -0,0 +1,5826 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "df2b52e76b29af7a124ff05c81b0c1fd", + "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, 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 + } + ], + "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, 'df2b52e76b29af7a124ff05c81b0c1fd')" + ] + } +} \ 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/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..9cc176cb95 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,40 @@ 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( + "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/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() }) ) } From 88c808b2cac44766467f48477fb7e669cd300fe9 Mon Sep 17 00:00:00 2001 From: "andras.maczak" Date: Wed, 17 Sep 2025 15:03:46 +0200 Subject: [PATCH 09/23] student assignment details screen changes --- .../src/main/java/GlobalDependencies.kt | 2 + .../canvasapi2/apis/AssignmentAPI.kt | 2 +- libs/pandares/src/main/res/values/strings.xml | 2 + libs/pandautils/build.gradle | 1 + .../12.json | 12 +- .../13.json | 722 ++++++++++++++++++ .../details/AssignmentDetailsFragment.kt | 22 +- .../details/AssignmentDetailsViewModel.kt | 46 +- .../composables/DueDateReminderLayout.kt | 72 ++ .../features/reminder/ReminderManager.kt | 11 +- .../features/reminder/ReminderRepository.kt | 6 +- .../features/reminder/ReminderViewState.kt | 2 + .../reminder/composables/ReminderView.kt | 6 +- .../room/appdatabase/AppDatabase.kt | 2 +- .../room/appdatabase/AppDatabaseMigrations.kt | 6 +- .../appdatabase/entities/ReminderEntity.kt | 3 +- .../layout/fragment_assignment_details.xml | 8 +- 17 files changed, 902 insertions(+), 23 deletions(-) create mode 100644 libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/13.json create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DueDateReminderLayout.kt diff --git a/apps/buildSrc/src/main/java/GlobalDependencies.kt b/apps/buildSrc/src/main/java/GlobalDependencies.kt index 7120f7076b..32ea95f1c3 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,6 +132,7 @@ 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" + const val COMPOSE_LIVEDATA = "androidx.compose.runtime:runtime-livedata:${Versions.LIVEDATA}" /* Media and content handling */ const val PSPDFKIT = "com.pspdfkit:pspdfkit:${Versions.PSPDFKIT}" 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..fb50ff4cc3 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, 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/build.gradle b/libs/pandautils/build.gradle index 9cce7f814b..f1277eae5b 100644 --- a/libs/pandautils/build.gradle +++ b/libs/pandautils/build.gradle @@ -260,4 +260,5 @@ dependencies { implementation Libs.LOTTIE_COMPOSE implementation Libs.DISK_LRU_CACHE + implementation Libs.COMPOSE_LIVEDATA } diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/12.json b/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/12.json index 32fc5f831d..eb2ff1c69c 100644 --- a/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/12.json +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/12.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 12, - "identityHash": "51a92e7d32ab68ff24af5213c6d6c162", + "identityHash": "d7eba14162e2c9edf9afca9a2e1b860e", "entities": [ { "tableName": "AttachmentEntity", @@ -502,7 +502,7 @@ }, { "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)", + "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 NOT NULL)", "fields": [ { "fieldPath": "id", @@ -545,6 +545,12 @@ "columnName": "time", "affinity": "INTEGER", "notNull": true + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": true } ], "primaryKey": { @@ -710,7 +716,7 @@ "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, '51a92e7d32ab68ff24af5213c6d6c162')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd7eba14162e2c9edf9afca9a2e1b860e')" ] } } \ No newline at end of file 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/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 0d9d3fbd64..89fa9aef29 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 @@ -154,6 +155,21 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo } ) } + binding?.dueComposeView?.setContent { + val states = viewModel.dueDatesViewState + DueDateReminderLayout( + states, + onAddClick = { checkAlarmPermission() }, + onRemoveClick = { reminderId -> + viewModel.showDeleteReminderConfirmationDialog( + requireContext(), + reminderId, + assignmentDetailsBehaviour.dialogColor + ) + } + ) + } + return binding?.root } @@ -365,14 +381,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( @@ -383,7 +399,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/AssignmentDetailsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt index 8125fd72d8..5e7e4919a7 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,10 +70,12 @@ 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 import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -136,6 +139,10 @@ class AssignmentDetailsViewModel @Inject constructor( private val _reminderViewState = MutableStateFlow(ReminderViewState()) val reminderViewState = _reminderViewState.asStateFlow() + private val _dueDatesViewState = mutableStateListOf() + val dueDatesViewState: List + get() = _dueDatesViewState + var checkingReminderPermission = false var checkingNotificationPermission = false @@ -161,9 +168,15 @@ class AssignmentDetailsViewModel @Inject constructor( dueDate = assignment?.dueDate ) } _data.value?.notifyPropertyChanged(BR.reminders) + + } } + private fun updateDueDatesViewState(reminderEntities: List) { + + } + fun getVideoUri(fragment: FragmentActivity): Uri? = submissionHandler.getVideoUri(fragment) override fun onCleared() { @@ -229,6 +242,31 @@ class AssignmentDetailsViewModel @Inject constructor( _reminderViewState.update { it.copy( dueDate = if (assignment?.submission?.excused.orDefault()) null else assignment?.dueDate ) } + + if (assignment?.checkpoints?.isNotEmpty() == true) { + _dueDatesViewState.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) + } + _dueDatesViewState.add( + ReminderViewState( + dueLabel = dueLabel, + themeColor = Color.Red, + dueDate = checkpoint.dueDate, + tag = checkpoint.tag, + ) + ) + } + } _data.postValue(getViewData(assignmentResult, hasDraft)) _state.postValue(ViewState.Success) } catch (ex: Exception) { @@ -611,7 +649,7 @@ class AssignmentDetailsViewModel @Inject constructor( _reminderViewState.update { it.copy(themeColor = Color(color)) } } - fun showCreateReminderDialog(context: Context, @ColorInt color: Int) { + fun showCreateReminderDialog(context: Context, @ColorInt color: Int, tag: String? = null) { assignment?.let { assignment -> viewModelScope.launch { when { @@ -621,7 +659,8 @@ class AssignmentDetailsViewModel @Inject constructor( assignment.id, assignment.name.orEmpty(), assignment.htmlUrl.orEmpty(), - assignment.dueDate + assignment.dueDate, + tag ) assignment.dueDate?.before(Date()).orDefault() -> reminderManager.showCustomReminderDialog( context, @@ -629,7 +668,8 @@ class AssignmentDetailsViewModel @Inject constructor( assignment.id, assignment.name.orEmpty(), assignment.htmlUrl.orEmpty(), - assignment.dueDate + assignment.dueDate, + tag ) else -> reminderManager.showBeforeDueDateReminderDialog( context, 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..78f62f2a0f --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DueDateReminderLayout.kt @@ -0,0 +1,72 @@ +/* + * 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.res.colorResource +import androidx.compose.ui.res.stringResource +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.padding(horizontal = 16.dp)) { + for (reminderViewState in reminderViewStates) { + DueDateBlock(reminderViewState) + ReminderView( + viewState = reminderViewState, + onAddClick = onAddClick, + onRemoveClick = onRemoveClick + ) + CanvasDivider() + } + } +} + +@Composable +private fun DueDateBlock( + reminderViewState: ReminderViewState +) { + Text( + modifier = Modifier.padding(top = 24.dp), + 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), + text = "${reminderViewState.dueDate?.toFormattedString()}", + 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..60ce6804cd 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 @@ -115,10 +115,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 +186,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 +222,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..7c30f758e8 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,7 +51,7 @@ import com.instructure.pandautils.utils.toFormattedString @Composable fun ReminderView( viewState: ReminderViewState, - onAddClick: () -> Unit, + onAddClick: (String?) -> Unit, onRemoveClick: (Long) -> Unit, ) { CanvasTheme { @@ -71,9 +71,9 @@ 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/res/layout/fragment_assignment_details.xml b/libs/pandautils/src/main/res/layout/fragment_assignment_details.xml index e7b42ce6cf..c84cfd61d1 100644 --- a/libs/pandautils/src/main/res/layout/fragment_assignment_details.xml +++ b/libs/pandautils/src/main/res/layout/fragment_assignment_details.xml @@ -279,6 +279,12 @@ android:visibility="@{viewModel.data.dueDate.empty ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toBottomOf="@id/lockedMessageTextView" /> + + + app:layout_constraintTop_toBottomOf="@id/dueComposeView" /> Date: Thu, 18 Sep 2025 11:17:20 +0200 Subject: [PATCH 10/23] updated reminder logic, removed unnecessary code --- .../details/AssignmentDetailsFragment.kt | 15 +--- .../details/AssignmentDetailsViewModel.kt | 76 ++++++++++++------- .../composables/DueDateReminderLayout.kt | 12 ++- .../features/reminder/ReminderManager.kt | 7 +- .../reminder/composables/ReminderView.kt | 4 +- .../layout/fragment_assignment_details.xml | 50 +----------- 6 files changed, 70 insertions(+), 94 deletions(-) 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 89fa9aef29..782db62b7f 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 @@ -145,21 +145,12 @@ 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() }, - onRemoveClick = { reminderId -> - viewModel.showDeleteReminderConfirmationDialog(requireContext(), reminderId, assignmentDetailsBehaviour.dialogColor) - } - ) - } + binding?.dueComposeView?.setContent { - val states = viewModel.dueDatesViewState + val states = viewModel.dueDateReminderViewStates DueDateReminderLayout( states, - onAddClick = { checkAlarmPermission() }, + onAddClick = { tag -> checkAlarmPermission(tag) }, onRemoveClick = { reminderId -> viewModel.showDeleteReminderConfirmationDialog( requireContext(), 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 5e7e4919a7..d25c6473de 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 @@ -75,7 +75,6 @@ import com.instructure.pandautils.utils.toFormattedString import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -136,12 +135,11 @@ class AssignmentDetailsViewModel @Inject constructor( private var selectedSubmission: Submission? = null - private val _reminderViewState = MutableStateFlow(ReminderViewState()) - val reminderViewState = _reminderViewState.asStateFlow() - - private val _dueDatesViewState = mutableStateListOf() - val dueDatesViewState: List - get() = _dueDatesViewState + 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 @@ -163,18 +161,29 @@ class AssignmentDetailsViewModel @Inject constructor( 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) @@ -239,12 +248,9 @@ 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) { - _dueDatesViewState.clear() + _dueDateReminderViewStates.clear() assignment?.orderedCheckpoints?.forEach { checkpoint -> val dueLabel = when (checkpoint.tag) { Const.REPLY_TO_TOPIC -> application.getString(R.string.reply_to_topic_due) @@ -257,15 +263,28 @@ class AssignmentDetailsViewModel @Inject constructor( else -> application.getString(R.string.dueLabel) } - _dueDatesViewState.add( + val subAssignment = assignment?.submission?.subAssignmentSubmissions?.firstOrNull { it.subAssignmentTag == checkpoint.tag } + _dueDateReminderViewStates.add( ReminderViewState( dueLabel = dueLabel, - themeColor = Color.Red, - dueDate = checkpoint.dueDate, + 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) @@ -646,29 +665,33 @@ 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, 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( @@ -677,8 +700,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 index 78f62f2a0f..3dda6cde12 100644 --- 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 @@ -24,6 +24,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier 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 @@ -39,7 +41,7 @@ fun DueDateReminderLayout( onRemoveClick: (Long) -> Unit, modifier: Modifier = Modifier ) { - Column(modifier = modifier.padding(horizontal = 16.dp)) { + Column { for (reminderViewState in reminderViewStates) { DueDateBlock(reminderViewState) ReminderView( @@ -57,15 +59,17 @@ private fun DueDateBlock( reminderViewState: ReminderViewState ) { Text( - modifier = Modifier.padding(top = 24.dp), + modifier = Modifier + .padding(top = 24.dp, start = 16.dp, end = 16.dp) + .semantics { heading() }, 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), - text = "${reminderViewState.dueDate?.toFormattedString()}", + modifier = Modifier.padding(bottom = 14.dp, start = 16.dp, end = 16.dp), + text = "${reminderViewState.dueDate?.toFormattedString() ?: stringResource(R.string.toDoNoDueDate)}", color = colorResource(id = R.color.textDarkest), fontSize = 16.sp ) 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 60ce6804cd..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), 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 7c30f758e8..f52f2e30cd 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 @@ -71,7 +71,9 @@ fun ReminderView( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .padding(vertical = 12.dp) - .clickable { onAddClick(viewState.tag) } + .clickable { + onAddClick(viewState.tag) + } ) { IconButton(onClick = { onAddClick(viewState.tag) }) { Icon( 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 c84cfd61d1..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/dueComposeView" /> Date: Fri, 19 Sep 2025 15:09:34 +0200 Subject: [PATCH 11/23] unit tests added --- .../details/AssignmentDetailsViewData.kt | 3 +- .../details/AssignmentDetailsViewModel.kt | 6 +- .../composables/DueDateReminderLayout.kt | 13 +- .../details/AssignmentDetailsViewModelTest.kt | 191 +++++++++++++++++- 4 files changed, 197 insertions(+), 16 deletions(-) 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 d25c6473de..89936ad3a7 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 @@ -160,9 +160,6 @@ class AssignmentDetailsViewModel @Inject constructor( loadData() reminderManager.observeRemindersLiveData(apiPrefs.user?.id.orDefault(), assignmentId) { reminderEntities -> - _data.value?.reminders = mapReminders(reminderEntities) - _data.value?.notifyPropertyChanged(BR.reminders) - this.reminderEntities = reminderEntities updateDueDatesViewState(reminderEntities) } @@ -515,8 +512,7 @@ class AssignmentDetailsViewModel @Inject constructor( discussionHeaderViewData = discussionHeaderViewData, quizDetails = quizViewViewData, attemptsViewData = attemptsViewData, - hasDraft = hasDraft, - reminders = _data.value?.reminders.orEmpty(), + hasDraft = hasDraft ) } 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 index 3dda6cde12..13d481c8c4 100644 --- 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 @@ -22,6 +22,7 @@ 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 @@ -41,9 +42,9 @@ fun DueDateReminderLayout( onRemoveClick: (Long) -> Unit, modifier: Modifier = Modifier ) { - Column { - for (reminderViewState in reminderViewStates) { - DueDateBlock(reminderViewState) + Column(modifier = modifier) { + reminderViewStates.forEachIndexed { index, reminderViewState -> + DueDateBlock(reminderViewState, index) ReminderView( viewState = reminderViewState, onAddClick = onAddClick, @@ -56,12 +57,14 @@ fun DueDateReminderLayout( @Composable private fun DueDateBlock( - reminderViewState: ReminderViewState + reminderViewState: ReminderViewState, + position: Int ) { Text( modifier = Modifier .padding(top = 24.dp, start = 16.dp, end = 16.dp) - .semantics { heading() }, + .semantics { heading() } + .testTag("dueDateHeaderText-$position"), text = reminderViewState.dueLabel ?: stringResource(id = R.string.dueLabel), color = colorResource(id = R.color.textDark), fontSize = 14.sp diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt index 3b30833801..e46b4442e7 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt @@ -30,11 +30,13 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import com.instructure.canvasapi2.CustomGradeStatusesQuery import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.Enrollment import com.instructure.canvasapi2.models.LockInfo import com.instructure.canvasapi2.models.Quiz +import com.instructure.canvasapi2.models.SubAssignmentSubmission import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.Analytics @@ -797,8 +799,71 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel(realReminderManager) assertEquals( - reminderEntities.map { ReminderViewData(it.id, it.text) }, - viewModel.data.value?.reminders?.map { it.data } + reminderEntities.map { it.id }, + viewModel.dueDateReminderViewStates[0].reminders.map { it.id } + ) + } + + @Test + fun `Reminders map correctly for discussion checkpoints`() { + val reminderEntities = listOf( + ReminderEntity(1, 1, 1, "htmlUrl1", "Assignment 1", "1 day", 1000, "reply_to_topic"), + ReminderEntity(2, 1, 1, "htmlUrl2", "Assignment 2", "2 days", 2000, "reply_to_topic"), + ReminderEntity(3, 1, 1, "htmlUrl3", "Assignment 3", "3 days", 3000, "reply_to_entry") + ) + val dateTimePicker: DateTimePicker = mockk(relaxed = true) + val reminderRepository: ReminderRepository = mockk(relaxed = true) + val realReminderManager = ReminderManager(dateTimePicker, reminderRepository, analytics) + + every { reminderRepository.findByAssignmentIdLiveData(any(), any()) } returns MutableLiveData(reminderEntities) + 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( + 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 } ) } @@ -822,11 +887,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 @@ -1008,4 +1142,53 @@ class AssignmentDetailsViewModelTest { ) } } + + @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()) + } } From c0ef2bcfb7025900b5f8e669b06968fca1fafa9f Mon Sep 17 00:00:00 2001 From: "andras.maczak" Date: Tue, 23 Sep 2025 14:52:04 +0200 Subject: [PATCH 12/23] added interaction tests --- .../AssignmentDetailsInteractionTest.kt | 70 +++++++++++++++++- .../ParentCalendarInteractionTest.kt | 2 +- .../parentapp/utils/ParentComposeTest.kt | 3 + .../instructure/parentapp/utils/ParentTest.kt | 3 - .../student/ui/e2e/classic/FilesE2ETest.kt | 11 +-- .../student/ui/e2e/classic/GradesE2ETest.kt | 4 +- .../student/ui/e2e/classic/ModulesE2ETest.kt | 4 +- .../ui/e2e/classic/ShareExtensionE2ETest.kt | 12 +--- .../e2e/classic/k5/ImportantDatesE2ETest.kt | 4 +- .../ui/e2e/classic/k5/ScheduleE2ETest.kt | 4 +- .../classic/offline/OfflineGradesE2ETest.kt | 6 +- .../classic/offline/OfflineModulesE2ETest.kt | 8 ++- .../AssignmentDetailsInteractionTest.kt | 72 ++++++++++++++++++- .../ui/interaction/ModuleInteractionTest.kt | 4 +- .../NotificationInteractionTest.kt | 4 +- .../PickerSubmissionUploadInteractionTest.kt | 14 ++-- .../ui/interaction/ScheduleInteractionTest.kt | 4 +- .../StudentCalendarInteractionTest.kt | 2 +- .../SubmissionDetailsInteractionTest.kt | 18 ++--- .../ui/interaction/TodoInteractionTest.kt | 4 +- .../classic/StudentAssignmentDetailsPage.kt | 3 +- .../student/ui/utils/StudentComposeTest.kt | 10 +++ .../student/ui/utils/StudentTest.kt | 53 +++++++++++++- .../interaction/GradesInteractionTest.kt | 2 +- .../interaction/SmartSearchInteractionTest.kt | 2 +- .../common/pages/AssignmentDetailsPage.kt | 17 +++-- .../canvas/espresso/mockcanvas/MockCanvas.kt | 50 ++++++++----- .../endpoints/AssignmentEndpoints.kt | 3 +- .../canvasapi2/apis/AssignmentAPI.kt | 2 +- .../composables/DueDateReminderLayout.kt | 9 ++- .../reminder/composables/ReminderView.kt | 3 +- 31 files changed, 313 insertions(+), 94 deletions(-) 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..5d4e36ba48 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,7 +19,6 @@ 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 @@ -27,31 +26,26 @@ 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.pressBackButton +import com.instructure.canvas.espresso.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType 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) } - @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..8e36ea5ffd 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 @@ -41,12 +41,16 @@ import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin import com.instructure.student.ui.utils.offline.OfflineTestUtils +import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest 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..67a8a75331 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 @@ -42,12 +42,18 @@ import com.instructure.student.ui.utils.extensions.tokenLogin import com.instructure.student.ui.utils.offline.OfflineTestUtils import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertOfflineIndicator import com.instructure.student.ui.utils.offline.OfflineTestUtils.waitForNetworkToGoOffline +import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils +import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator +import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest 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..3504247b87 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 @@ -45,11 +44,15 @@ import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvas.espresso.mockcanvas.addAssignment import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockCanvas.addAssignment +import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager +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.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 +67,7 @@ import java.io.File @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class PickerSubmissionUploadInteractionTest : StudentTest() { +class PickerSubmissionUploadInteractionTest : StudentComposeTest() { @BindValue @JvmField @@ -76,11 +79,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..0f48582ef3 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 @@ -18,6 +18,7 @@ package com.instructure.student.ui.interaction import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.test.junit4.createEmptyComposeRule +import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.web.webdriver.Locator import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils @@ -35,6 +36,13 @@ import com.instructure.canvas.espresso.mockcanvas.addRubricToAssignment import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockCanvas.addAssignment +import com.instructure.canvas.espresso.mockCanvas.addFileToCourse +import com.instructure.canvas.espresso.mockCanvas.addRubricToAssignment +import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager +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 @@ -46,19 +54,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 +75,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..ecf97c4c14 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.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..c21b2c45ff 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 @@ -101,6 +101,58 @@ import com.instructure.student.ui.pages.classic.offline.ManageOfflineContentPage import com.instructure.student.ui.pages.classic.offline.NativeDiscussionDetailsPage import com.instructure.student.ui.pages.classic.offline.OfflineSyncSettingsPage import com.instructure.student.ui.pages.classic.offline.SyncProgressPage +import com.instructure.student.espresso.TestAppManager +import com.instructure.student.ui.pages.AllCoursesPage +import com.instructure.student.ui.pages.AnnotationCommentListPage +import com.instructure.student.ui.pages.AnnouncementListPage +import com.instructure.student.ui.pages.BookmarkPage +import com.instructure.student.ui.pages.CanvasWebViewPage +import com.instructure.student.ui.pages.ConferenceDetailsPage +import com.instructure.student.ui.pages.ConferenceListPage +import com.instructure.student.ui.pages.CourseBrowserPage +import com.instructure.student.ui.pages.CourseGradesPage +import com.instructure.student.ui.pages.DashboardPage +import com.instructure.student.ui.pages.DiscussionListPage +import com.instructure.student.ui.pages.ElementaryCoursePage +import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.FileChooserPage +import com.instructure.student.ui.pages.FileListPage +import com.instructure.student.ui.pages.GoToQuizPage +import com.instructure.student.ui.pages.GradesPage +import com.instructure.student.ui.pages.GroupBrowserPage +import com.instructure.student.ui.pages.HelpPage +import com.instructure.student.ui.pages.HomeroomPage +import com.instructure.student.ui.pages.ImportantDatesPage +import com.instructure.student.ui.pages.LeftSideNavigationDrawerPage +import com.instructure.student.ui.pages.ModuleProgressionPage +import com.instructure.student.ui.pages.ModulesPage +import com.instructure.student.ui.pages.NotificationPage +import com.instructure.student.ui.pages.PageDetailsPage +import com.instructure.student.ui.pages.PageListPage +import com.instructure.student.ui.pages.PairObserverPage +import com.instructure.student.ui.pages.PandaAvatarPage +import com.instructure.student.ui.pages.PeopleListPage +import com.instructure.student.ui.pages.PersonDetailsPage +import com.instructure.student.ui.pages.PickerSubmissionUploadPage +import com.instructure.student.ui.pages.ProfileSettingsPage +import com.instructure.student.ui.pages.PushNotificationsPage +import com.instructure.student.ui.pages.QRLoginPage +import com.instructure.student.ui.pages.QuizListPage +import com.instructure.student.ui.pages.QuizTakingPage +import com.instructure.student.ui.pages.RemoteConfigSettingsPage +import com.instructure.student.ui.pages.ResourcesPage +import com.instructure.student.ui.pages.SchedulePage +import com.instructure.student.ui.pages.ShareExtensionStatusPage +import com.instructure.student.ui.pages.ShareExtensionTargetPage +import com.instructure.student.ui.pages.SubmissionDetailsPage +import com.instructure.student.ui.pages.SyllabusPage +import com.instructure.student.ui.pages.TextSubmissionUploadPage +import com.instructure.student.ui.pages.TodoPage +import com.instructure.student.ui.pages.UrlSubmissionUploadPage +import com.instructure.student.ui.pages.offline.ManageOfflineContentPage +import com.instructure.student.ui.pages.offline.NativeDiscussionDetailsPage +import com.instructure.student.ui.pages.offline.OfflineSyncSettingsPage +import com.instructure.student.ui.pages.offline.SyncProgressPage import instructure.rceditor.RCETextEditor import org.hamcrest.Matcher import org.hamcrest.core.AllOf @@ -120,7 +172,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..b137b4db7f 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,12 @@ package com.instructure.canvas.espresso.common.pages import android.view.View import android.widget.ScrollView +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag import androidx.test.espresso.AmbiguousViewMatcherException import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onData @@ -68,10 +74,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 +91,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 +259,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").assertIsDisplayed() } 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 fb50ff4cc3..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 @@ -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/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 index 13d481c8c4..b7c7705ac0 100644 --- 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 @@ -48,7 +48,8 @@ fun DueDateReminderLayout( ReminderView( viewState = reminderViewState, onAddClick = onAddClick, - onRemoveClick = onRemoveClick + onRemoveClick = onRemoveClick, + modifier = Modifier.testTag("reminderView-$index") ) CanvasDivider() } @@ -71,8 +72,10 @@ private fun DueDateBlock( ) Spacer(modifier = Modifier.height(2.dp)) Text( - modifier = Modifier.padding(bottom = 14.dp, start = 16.dp, end = 16.dp), - text = "${reminderViewState.dueDate?.toFormattedString() ?: stringResource(R.string.toDoNoDueDate)}", + 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 ) 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 f52f2e30cd..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, + modifier: Modifier = Modifier, onAddClick: (String?) -> Unit, onRemoveClick: (Long) -> Unit, ) { CanvasTheme { Column( - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding(vertical = 24.dp, horizontal = 16.dp) ) { From fb83e49d01a7d0cedbcde9f68a8f08e32bd6c8a4 Mon Sep 17 00:00:00 2001 From: "andras.maczak" Date: Tue, 23 Sep 2025 21:20:43 +0200 Subject: [PATCH 13/23] fixed tests --- .../parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt | 2 +- .../canvas/espresso/common/pages/AssignmentDetailsPage.kt | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) 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/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 b137b4db7f..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,11 +18,9 @@ package com.instructure.canvas.espresso.common.pages import android.view.View import android.widget.ScrollView -import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onNodeWithTag import androidx.test.espresso.AmbiguousViewMatcherException import androidx.test.espresso.Espresso @@ -260,7 +258,7 @@ open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteracti } fun assertReminderViewDisplayed(position: Int = 0) { - composeTestRule.onNodeWithTag("reminderView-$position").assertIsDisplayed() + composeTestRule.onNodeWithTag("reminderView-$position").assertExists() } fun assertNoDescriptionViewDisplayed() { From 4da6537da369a6a1a222cf3aea84bf1c8360ce1f Mon Sep 17 00:00:00 2001 From: "andras.maczak" Date: Wed, 24 Sep 2025 13:46:54 +0200 Subject: [PATCH 14/23] fixed tests --- .../student/ui/interaction/SubmissionDetailsInteractionTest.kt | 2 ++ .../main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) 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 0f48582ef3..2fe4b295bd 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 @@ -78,6 +78,7 @@ class SubmissionDetailsInteractionTest : StudentComposeTest() { // Should be able to add a comment on a submission @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) + @Stub fun testComments_addCommentToSingleAttemptSubmission() { val data = getToCourse() @@ -103,6 +104,7 @@ class SubmissionDetailsInteractionTest : StudentComposeTest() { @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) + @Stub fun testComments_addCommentToMultipleAttemptSubmission() { val data = getToCourse() val assignment = data.addAssignment( diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt index aeeb44a087..6ee3e98c98 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt @@ -32,7 +32,7 @@ class ScreenshotTestRule : TestRule { // Run all test methods tryCount times. Take screenshots on failure. // A method rule would allow targeting specific (method.getAnnotation(Retry.class)) - private val tryCount = 5 + private val tryCount = 1 override fun apply(base: Statement, description: Description): Statement { return object : Statement() { From 6a512fbe62bbb9d82418544e308b2684b613e53d Mon Sep 17 00:00:00 2001 From: "andras.maczak" Date: Wed, 24 Sep 2025 13:47:46 +0200 Subject: [PATCH 15/23] tryCount change reverted --- .../main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt index 6ee3e98c98..aeeb44a087 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt @@ -32,7 +32,7 @@ class ScreenshotTestRule : TestRule { // Run all test methods tryCount times. Take screenshots on failure. // A method rule would allow targeting specific (method.getAnnotation(Retry.class)) - private val tryCount = 1 + private val tryCount = 5 override fun apply(base: Statement, description: Description): Statement { return object : Statement() { From 927868cc263d27cd062f450824e8299f111a2d10 Mon Sep 17 00:00:00 2001 From: "kristof.nemere" Date: Tue, 7 Oct 2025 15:35:03 +0200 Subject: [PATCH 16/23] Offline DCP --- .../6.json | 5826 +++++++++++++++++ .../room/offline/daos/CheckpointDaoTest.kt | 231 + .../daos/SubAssignmentSubmissionDaoTest.kt | 342 + .../pandautils/di/OfflineModule.kt | 20 +- .../room/offline/OfflineDatabase.kt | 14 +- .../room/offline/OfflineDatabaseMigrations.kt | 35 + .../room/offline/daos/CheckpointDao.kt | 40 + .../daos/SubAssignmentSubmissionDao.kt | 40 + .../room/offline/entities/AssignmentEntity.kt | 6 +- .../room/offline/entities/CheckpointEntity.kt | 68 + .../entities/SubAssignmentSubmissionEntity.kt | 84 + .../room/offline/entities/SubmissionEntity.kt | 14 +- .../room/offline/facade/AssignmentFacade.kt | 9 +- .../room/offline/facade/SubmissionFacade.kt | 11 +- 14 files changed, 6727 insertions(+), 13 deletions(-) create mode 100644 libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/6.json create mode 100644 libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CheckpointDaoTest.kt create mode 100644 libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDaoTest.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CheckpointDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CheckpointEntity.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubAssignmentSubmissionEntity.kt 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..fc3d9231c7 --- /dev/null +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/6.json @@ -0,0 +1,5826 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "df2b52e76b29af7a124ff05c81b0c1fd", + "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, 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 + } + ], + "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, 'df2b52e76b29af7a124ff05c81b0c1fd')" + ] + } +} \ 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/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..9cc176cb95 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,40 @@ 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( + "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/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() }) ) } From 58bc08c183006d9756091c7107e558751caaa879 Mon Sep 17 00:00:00 2001 From: "kristof.nemere" Date: Wed, 8 Oct 2025 09:06:23 +0200 Subject: [PATCH 17/23] test fixes --- .../ui/e2e/classic/ShareExtensionE2ETest.kt | 2 - .../classic/offline/OfflineGradesE2ETest.kt | 6 +-- .../classic/offline/OfflineModulesE2ETest.kt | 8 +-- .../PickerSubmissionUploadInteractionTest.kt | 5 -- .../SubmissionDetailsInteractionTest.kt | 10 ---- .../student/ui/utils/StudentComposeTest.kt | 2 +- .../student/ui/utils/StudentTest.kt | 53 ------------------- .../offline/facade/AssignmentFacadeTest.kt | 13 +++++ .../offline/facade/SubmissionFacadeTest.kt | 10 +++- 9 files changed, 25 insertions(+), 84 deletions(-) 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 5d4e36ba48..818952275f 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 @@ -24,9 +24,7 @@ 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.pressBackButton -import com.instructure.canvas.espresso.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType import com.instructure.dataseeding.model.SubmissionType 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 8e36ea5ffd..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,14 +37,10 @@ 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 -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils -import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test 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 67a8a75331..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,18 +36,12 @@ 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 import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertOfflineIndicator import com.instructure.student.ui.utils.offline.OfflineTestUtils.waitForNetworkToGoOffline -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline -import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test 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 3504247b87..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 @@ -39,15 +39,10 @@ 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 import com.instructure.canvas.espresso.mockcanvas.init -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -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 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 2fe4b295bd..45cc43cc9d 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,8 +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.Espresso import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.web.webdriver.Locator import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils @@ -28,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 @@ -36,13 +33,6 @@ import com.instructure.canvas.espresso.mockcanvas.addRubricToAssignment import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager import com.instructure.canvas.espresso.mockcanvas.init -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addRubricToAssignment -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -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 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 ecf97c4c14..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 @@ -38,7 +38,7 @@ import com.instructure.canvas.espresso.common.pages.compose.SmartSearchPreferenc import com.instructure.espresso.ModuleItemInteractions import com.instructure.student.R import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.StudentAssignmentDetailsPage +import com.instructure.student.ui.pages.classic.StudentAssignmentDetailsPage import org.junit.Rule abstract class StudentComposeTest : StudentTest() { 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 c21b2c45ff..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 @@ -101,58 +100,6 @@ import com.instructure.student.ui.pages.classic.offline.ManageOfflineContentPage import com.instructure.student.ui.pages.classic.offline.NativeDiscussionDetailsPage import com.instructure.student.ui.pages.classic.offline.OfflineSyncSettingsPage import com.instructure.student.ui.pages.classic.offline.SyncProgressPage -import com.instructure.student.espresso.TestAppManager -import com.instructure.student.ui.pages.AllCoursesPage -import com.instructure.student.ui.pages.AnnotationCommentListPage -import com.instructure.student.ui.pages.AnnouncementListPage -import com.instructure.student.ui.pages.BookmarkPage -import com.instructure.student.ui.pages.CanvasWebViewPage -import com.instructure.student.ui.pages.ConferenceDetailsPage -import com.instructure.student.ui.pages.ConferenceListPage -import com.instructure.student.ui.pages.CourseBrowserPage -import com.instructure.student.ui.pages.CourseGradesPage -import com.instructure.student.ui.pages.DashboardPage -import com.instructure.student.ui.pages.DiscussionListPage -import com.instructure.student.ui.pages.ElementaryCoursePage -import com.instructure.student.ui.pages.ElementaryDashboardPage -import com.instructure.student.ui.pages.FileChooserPage -import com.instructure.student.ui.pages.FileListPage -import com.instructure.student.ui.pages.GoToQuizPage -import com.instructure.student.ui.pages.GradesPage -import com.instructure.student.ui.pages.GroupBrowserPage -import com.instructure.student.ui.pages.HelpPage -import com.instructure.student.ui.pages.HomeroomPage -import com.instructure.student.ui.pages.ImportantDatesPage -import com.instructure.student.ui.pages.LeftSideNavigationDrawerPage -import com.instructure.student.ui.pages.ModuleProgressionPage -import com.instructure.student.ui.pages.ModulesPage -import com.instructure.student.ui.pages.NotificationPage -import com.instructure.student.ui.pages.PageDetailsPage -import com.instructure.student.ui.pages.PageListPage -import com.instructure.student.ui.pages.PairObserverPage -import com.instructure.student.ui.pages.PandaAvatarPage -import com.instructure.student.ui.pages.PeopleListPage -import com.instructure.student.ui.pages.PersonDetailsPage -import com.instructure.student.ui.pages.PickerSubmissionUploadPage -import com.instructure.student.ui.pages.ProfileSettingsPage -import com.instructure.student.ui.pages.PushNotificationsPage -import com.instructure.student.ui.pages.QRLoginPage -import com.instructure.student.ui.pages.QuizListPage -import com.instructure.student.ui.pages.QuizTakingPage -import com.instructure.student.ui.pages.RemoteConfigSettingsPage -import com.instructure.student.ui.pages.ResourcesPage -import com.instructure.student.ui.pages.SchedulePage -import com.instructure.student.ui.pages.ShareExtensionStatusPage -import com.instructure.student.ui.pages.ShareExtensionTargetPage -import com.instructure.student.ui.pages.SubmissionDetailsPage -import com.instructure.student.ui.pages.SyllabusPage -import com.instructure.student.ui.pages.TextSubmissionUploadPage -import com.instructure.student.ui.pages.TodoPage -import com.instructure.student.ui.pages.UrlSubmissionUploadPage -import com.instructure.student.ui.pages.offline.ManageOfflineContentPage -import com.instructure.student.ui.pages.offline.NativeDiscussionDetailsPage -import com.instructure.student.ui.pages.offline.OfflineSyncSettingsPage -import com.instructure.student.ui.pages.offline.SyncProgressPage import instructure.rceditor.RCETextEditor import org.hamcrest.Matcher import org.hamcrest.core.AllOf 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)) From 2617b4554874f57f1e6b4493afd78971284da5b0 Mon Sep 17 00:00:00 2001 From: "kristof.nemere" Date: Wed, 8 Oct 2025 09:08:48 +0200 Subject: [PATCH 18/23] Unstubbed already fixed tests --- .../student/ui/interaction/SubmissionDetailsInteractionTest.kt | 2 -- 1 file changed, 2 deletions(-) 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 45cc43cc9d..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 @@ -68,7 +68,6 @@ class SubmissionDetailsInteractionTest : StudentComposeTest() { // Should be able to add a comment on a submission @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) - @Stub fun testComments_addCommentToSingleAttemptSubmission() { val data = getToCourse() @@ -94,7 +93,6 @@ class SubmissionDetailsInteractionTest : StudentComposeTest() { @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) - @Stub fun testComments_addCommentToMultipleAttemptSubmission() { val data = getToCourse() val assignment = data.addAssignment( From ffb186479f1c530a63c360f7dd412e73ebae65d8 Mon Sep 17 00:00:00 2001 From: "kristof.nemere" Date: Wed, 8 Oct 2025 15:15:42 +0200 Subject: [PATCH 19/23] Comment fixes --- apps/buildSrc/src/main/java/GlobalDependencies.kt | 2 -- libs/pandautils/build.gradle | 1 - .../12.json | 12 +++--------- .../details/AssignmentDetailsViewModelTest.kt | 1 + 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/apps/buildSrc/src/main/java/GlobalDependencies.kt b/apps/buildSrc/src/main/java/GlobalDependencies.kt index 32ea95f1c3..a10251eb0d 100644 --- a/apps/buildSrc/src/main/java/GlobalDependencies.kt +++ b/apps/buildSrc/src/main/java/GlobalDependencies.kt @@ -132,8 +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" - const val COMPOSE_LIVEDATA = "androidx.compose.runtime:runtime-livedata:${Versions.LIVEDATA}" - /* Media and content handling */ const val PSPDFKIT = "com.pspdfkit:pspdfkit:${Versions.PSPDFKIT}" const val MEDIA3 = "androidx.media3:media3-exoplayer:${Versions.MEDIA3}" diff --git a/libs/pandautils/build.gradle b/libs/pandautils/build.gradle index f1277eae5b..9cce7f814b 100644 --- a/libs/pandautils/build.gradle +++ b/libs/pandautils/build.gradle @@ -260,5 +260,4 @@ dependencies { implementation Libs.LOTTIE_COMPOSE implementation Libs.DISK_LRU_CACHE - implementation Libs.COMPOSE_LIVEDATA } diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/12.json b/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/12.json index eb2ff1c69c..32fc5f831d 100644 --- a/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/12.json +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/12.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 12, - "identityHash": "d7eba14162e2c9edf9afca9a2e1b860e", + "identityHash": "51a92e7d32ab68ff24af5213c6d6c162", "entities": [ { "tableName": "AttachmentEntity", @@ -502,7 +502,7 @@ }, { "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 NOT NULL)", + "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)", "fields": [ { "fieldPath": "id", @@ -545,12 +545,6 @@ "columnName": "time", "affinity": "INTEGER", "notNull": true - }, - { - "fieldPath": "tag", - "columnName": "tag", - "affinity": "TEXT", - "notNull": true } ], "primaryKey": { @@ -716,7 +710,7 @@ "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, 'd7eba14162e2c9edf9afca9a2e1b860e')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '51a92e7d32ab68ff24af5213c6d6c162')" ] } } \ No newline at end of file diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt index e89d7f5539..0943a58f14 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt @@ -1,3 +1,4 @@ + /* * Copyright (C) 2023 - present Instructure, Inc. * From 5b2372df6d0b6037eef6e5410243558e5ddd9ecc Mon Sep 17 00:00:00 2001 From: "kristof.nemere" Date: Thu, 9 Oct 2025 10:04:53 +0200 Subject: [PATCH 20/23] Added replyRequiredCount to DiscussionTopicHeaderEntity.kt --- .../6.json | 12 +++++++++--- .../room/offline/OfflineDatabaseMigrations.kt | 1 + .../offline/entities/DiscussionTopicHeaderEntity.kt | 7 +++++-- 3 files changed, 15 insertions(+), 5 deletions(-) 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 index fc3d9231c7..8bfb3ebb8f 100644 --- a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/6.json +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/6.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 6, - "identityHash": "df2b52e76b29af7a124ff05c81b0c1fd", + "identityHash": "e0e8981a53e92176b25c0fb1066137d6", "entities": [ { "tableName": "AssignmentDueDateEntity", @@ -1294,7 +1294,7 @@ }, { "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, 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 )", + "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", @@ -1493,6 +1493,12 @@ "columnName": "anonymousState", "affinity": "TEXT", "notNull": false + }, + { + "fieldPath": "replyRequiredCount", + "columnName": "replyRequiredCount", + "affinity": "INTEGER", + "notNull": false } ], "primaryKey": { @@ -5820,7 +5826,7 @@ "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, 'df2b52e76b29af7a124ff05c81b0c1fd')" + "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/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt index 9cc176cb95..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 @@ -122,6 +122,7 @@ val offlineDatabaseMigrations = arrayOf( }, 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," + 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 From c0360456168a8cb386d47a72cdb45f321d29f864 Mon Sep 17 00:00:00 2001 From: "kristof.deak" Date: Fri, 10 Oct 2025 14:34:17 +0200 Subject: [PATCH 21/23] Attempt to fix ShareExtensionE2ETest refs: MBL-19126 affects: Student, Teacher, Parent release note: --- .../ui/e2e/classic/ShareExtensionE2ETest.kt | 4 +++ .../file/upload/FileUploadDialogViewModel.kt | 6 ++-- .../file/upload/worker/FileUploadWorker.kt | 30 +++++++++++++++---- .../ShareExtensionProgressDialogViewModel.kt | 12 ++++++-- .../comments/SpeedGraderCommentsViewModel.kt | 1 + 5 files changed, 42 insertions(+), 11 deletions(-) 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 818952275f..f0d1555f76 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 @@ -31,6 +31,8 @@ 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.espresso.handleWorkManagerTask +import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin @@ -102,6 +104,7 @@ class ShareExtensionE2ETest: StudentComposeTest() { Log.d(STEP_TAG, "Click on 'Turn In' button to upload both of the files.") fileChooserPage.clickTurnIn() + handleWorkManagerTask(FileUploadWorker.WORKER_TAG) Log.d(ASSERTION_TAG, "Assert that the submission upload was successful.") shareExtensionStatusPage.assertPageObjects(30) @@ -175,6 +178,7 @@ class ShareExtensionE2ETest: StudentComposeTest() { Log.d(STEP_TAG, "Click on 'Upload' button to upload the file.") fileChooserPage.clickUpload() + handleWorkManagerTask(FileUploadWorker.WORKER_TAG) Log.d(ASSERTION_TAG, "Assert that the file upload (into my 'Files') was successful.") shareExtensionStatusPage.assertPageObjects() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt index d2eda653f3..37d448e1d7 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt @@ -38,7 +38,7 @@ import com.instructure.pandautils.utils.orDefault import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.io.File -import java.util.* +import java.util.UUID import javax.inject.Inject @HiltViewModel @@ -292,7 +292,9 @@ class FileUploadDialogViewModel @Inject constructor( if (uploadType == FileUploadType.DISCUSSION) { _events.value = Event(FileUploadAction.AttachmentSelectedAction(FileUploadDialogFragment.EVENT_ON_FILE_SELECTED, getAttachmentUri())) } else { - val worker = OneTimeWorkRequestBuilder().build() + val worker = OneTimeWorkRequestBuilder() + .addTag(FileUploadWorker.WORKER_TAG) + .build() val input = getInputData(worker.id, uris) fileUploadInputDao.insert(input) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt index 1d6f2c845d..3d863b8b85 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt @@ -24,8 +24,16 @@ import android.net.Uri import android.os.Build import androidx.core.app.NotificationCompat import androidx.hilt.work.HiltWorker -import androidx.work.* -import com.instructure.canvasapi2.managers.* +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.ForegroundInfo +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import com.instructure.canvasapi2.managers.AssignmentManager +import com.instructure.canvasapi2.managers.FileUploadConfig +import com.instructure.canvasapi2.managers.FileUploadManager +import com.instructure.canvasapi2.managers.GroupManager +import com.instructure.canvasapi2.managers.SubmissionManager import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.Submission @@ -37,15 +45,24 @@ import com.instructure.canvasapi2.utils.ProgressRequestUpdateListener import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.pandautils.R import com.instructure.pandautils.features.file.upload.FileUploadUtilsHelper -import com.instructure.pandautils.room.appdatabase.daos.* -import com.instructure.pandautils.room.appdatabase.entities.* +import com.instructure.pandautils.room.appdatabase.daos.AttachmentDao +import com.instructure.pandautils.room.appdatabase.daos.AuthorDao +import com.instructure.pandautils.room.appdatabase.daos.DashboardFileUploadDao +import com.instructure.pandautils.room.appdatabase.daos.FileUploadInputDao +import com.instructure.pandautils.room.appdatabase.daos.MediaCommentDao +import com.instructure.pandautils.room.appdatabase.daos.SubmissionCommentDao +import com.instructure.pandautils.room.appdatabase.entities.AttachmentEntity +import com.instructure.pandautils.room.appdatabase.entities.AuthorEntity +import com.instructure.pandautils.room.appdatabase.entities.DashboardFileUploadEntity +import com.instructure.pandautils.room.appdatabase.entities.FileUploadInputEntity +import com.instructure.pandautils.room.appdatabase.entities.MediaCommentEntity +import com.instructure.pandautils.room.appdatabase.entities.SubmissionCommentEntity import com.instructure.pandautils.utils.FileUploadUtils import com.instructure.pandautils.utils.orDefault import com.instructure.pandautils.utils.toJson import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import java.lang.IllegalStateException -import java.util.* +import java.util.Locale @HiltWorker class FileUploadWorker @AssistedInject constructor( @@ -438,6 +455,7 @@ class FileUploadWorker @AssistedInject constructor( const val FILE_SUBMIT_ACTION = "fileSubmitAction" const val FILE_PATHS = "filePaths" const val CHANNEL_ID = "uploadChannel" + const val WORKER_TAG = "FileUploadWorker" const val ACTION_ASSIGNMENT_SUBMISSION = "ACTION_ASSIGNMENT_SUBMISSION" const val ACTION_MESSAGE_ATTACHMENTS = "ACTION_MESSAGE_ATTACHMENTS" diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogViewModel.kt index c88be6a8f3..791f8cd8f3 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogViewModel.kt @@ -8,7 +8,11 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.work.* +import androidx.work.Data +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.hasKeyWithValueOfType import com.instructure.canvasapi2.models.postmodels.FileSubmitObject import com.instructure.pandautils.BR import com.instructure.pandautils.R @@ -26,7 +30,7 @@ import com.instructure.pandautils.utils.orDefault import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.io.File -import java.util.* +import java.util.UUID import javax.inject.Inject @HiltViewModel @@ -220,7 +224,9 @@ class ShareExtensionProgressDialogViewModel @Inject constructor( fun onRetryClick() { viewModelScope.launch { fileUploadInputDao.findByWorkerId(workerId.toString())?.let { - val worker = OneTimeWorkRequestBuilder().build() + val worker = OneTimeWorkRequestBuilder() + .addTag(FileUploadWorker.WORKER_TAG) + .build() fileUploadInputDao.insert(it.copy(workerId = worker.id.toString())) fileUploadInputDao.delete(it) dashboardFileUploadDao.deleteByWorkerId(it.workerId) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModel.kt index 8b20af362f..612dfae5a6 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModel.kt @@ -632,6 +632,7 @@ class SpeedGraderCommentsViewModel @Inject constructor( private fun restartWorker(filePaths: List) { viewModelScope.launch { val worker = OneTimeWorkRequestBuilder() + .addTag(FileUploadWorker.WORKER_TAG) .build() val inputData = FileUploadInputEntity( From c92cf3fe6260540a9525ad07298fe9a997005ff4 Mon Sep 17 00:00:00 2001 From: "kristof.nemere" Date: Fri, 10 Oct 2025 18:25:13 +0200 Subject: [PATCH 22/23] Revert "Attempt to fix ShareExtensionE2ETest" This reverts commit c0360456168a8cb386d47a72cdb45f321d29f864. --- .../ui/e2e/classic/ShareExtensionE2ETest.kt | 4 --- .../file/upload/FileUploadDialogViewModel.kt | 6 ++-- .../file/upload/worker/FileUploadWorker.kt | 30 ++++--------------- .../ShareExtensionProgressDialogViewModel.kt | 12 ++------ .../comments/SpeedGraderCommentsViewModel.kt | 1 - 5 files changed, 11 insertions(+), 42 deletions(-) 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 f0d1555f76..818952275f 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 @@ -31,8 +31,6 @@ 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.espresso.handleWorkManagerTask -import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin @@ -104,7 +102,6 @@ class ShareExtensionE2ETest: StudentComposeTest() { Log.d(STEP_TAG, "Click on 'Turn In' button to upload both of the files.") fileChooserPage.clickTurnIn() - handleWorkManagerTask(FileUploadWorker.WORKER_TAG) Log.d(ASSERTION_TAG, "Assert that the submission upload was successful.") shareExtensionStatusPage.assertPageObjects(30) @@ -178,7 +175,6 @@ class ShareExtensionE2ETest: StudentComposeTest() { Log.d(STEP_TAG, "Click on 'Upload' button to upload the file.") fileChooserPage.clickUpload() - handleWorkManagerTask(FileUploadWorker.WORKER_TAG) Log.d(ASSERTION_TAG, "Assert that the file upload (into my 'Files') was successful.") shareExtensionStatusPage.assertPageObjects() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt index 37d448e1d7..d2eda653f3 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt @@ -38,7 +38,7 @@ import com.instructure.pandautils.utils.orDefault import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.io.File -import java.util.UUID +import java.util.* import javax.inject.Inject @HiltViewModel @@ -292,9 +292,7 @@ class FileUploadDialogViewModel @Inject constructor( if (uploadType == FileUploadType.DISCUSSION) { _events.value = Event(FileUploadAction.AttachmentSelectedAction(FileUploadDialogFragment.EVENT_ON_FILE_SELECTED, getAttachmentUri())) } else { - val worker = OneTimeWorkRequestBuilder() - .addTag(FileUploadWorker.WORKER_TAG) - .build() + val worker = OneTimeWorkRequestBuilder().build() val input = getInputData(worker.id, uris) fileUploadInputDao.insert(input) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt index 3d863b8b85..1d6f2c845d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt @@ -24,16 +24,8 @@ import android.net.Uri import android.os.Build import androidx.core.app.NotificationCompat import androidx.hilt.work.HiltWorker -import androidx.work.CoroutineWorker -import androidx.work.Data -import androidx.work.ForegroundInfo -import androidx.work.WorkerParameters -import androidx.work.workDataOf -import com.instructure.canvasapi2.managers.AssignmentManager -import com.instructure.canvasapi2.managers.FileUploadConfig -import com.instructure.canvasapi2.managers.FileUploadManager -import com.instructure.canvasapi2.managers.GroupManager -import com.instructure.canvasapi2.managers.SubmissionManager +import androidx.work.* +import com.instructure.canvasapi2.managers.* import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.Submission @@ -45,24 +37,15 @@ import com.instructure.canvasapi2.utils.ProgressRequestUpdateListener import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.pandautils.R import com.instructure.pandautils.features.file.upload.FileUploadUtilsHelper -import com.instructure.pandautils.room.appdatabase.daos.AttachmentDao -import com.instructure.pandautils.room.appdatabase.daos.AuthorDao -import com.instructure.pandautils.room.appdatabase.daos.DashboardFileUploadDao -import com.instructure.pandautils.room.appdatabase.daos.FileUploadInputDao -import com.instructure.pandautils.room.appdatabase.daos.MediaCommentDao -import com.instructure.pandautils.room.appdatabase.daos.SubmissionCommentDao -import com.instructure.pandautils.room.appdatabase.entities.AttachmentEntity -import com.instructure.pandautils.room.appdatabase.entities.AuthorEntity -import com.instructure.pandautils.room.appdatabase.entities.DashboardFileUploadEntity -import com.instructure.pandautils.room.appdatabase.entities.FileUploadInputEntity -import com.instructure.pandautils.room.appdatabase.entities.MediaCommentEntity -import com.instructure.pandautils.room.appdatabase.entities.SubmissionCommentEntity +import com.instructure.pandautils.room.appdatabase.daos.* +import com.instructure.pandautils.room.appdatabase.entities.* import com.instructure.pandautils.utils.FileUploadUtils import com.instructure.pandautils.utils.orDefault import com.instructure.pandautils.utils.toJson import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import java.util.Locale +import java.lang.IllegalStateException +import java.util.* @HiltWorker class FileUploadWorker @AssistedInject constructor( @@ -455,7 +438,6 @@ class FileUploadWorker @AssistedInject constructor( const val FILE_SUBMIT_ACTION = "fileSubmitAction" const val FILE_PATHS = "filePaths" const val CHANNEL_ID = "uploadChannel" - const val WORKER_TAG = "FileUploadWorker" const val ACTION_ASSIGNMENT_SUBMISSION = "ACTION_ASSIGNMENT_SUBMISSION" const val ACTION_MESSAGE_ATTACHMENTS = "ACTION_MESSAGE_ATTACHMENTS" diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogViewModel.kt index 791f8cd8f3..c88be6a8f3 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogViewModel.kt @@ -8,11 +8,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.work.Data -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkInfo -import androidx.work.WorkManager -import androidx.work.hasKeyWithValueOfType +import androidx.work.* import com.instructure.canvasapi2.models.postmodels.FileSubmitObject import com.instructure.pandautils.BR import com.instructure.pandautils.R @@ -30,7 +26,7 @@ import com.instructure.pandautils.utils.orDefault import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.io.File -import java.util.UUID +import java.util.* import javax.inject.Inject @HiltViewModel @@ -224,9 +220,7 @@ class ShareExtensionProgressDialogViewModel @Inject constructor( fun onRetryClick() { viewModelScope.launch { fileUploadInputDao.findByWorkerId(workerId.toString())?.let { - val worker = OneTimeWorkRequestBuilder() - .addTag(FileUploadWorker.WORKER_TAG) - .build() + val worker = OneTimeWorkRequestBuilder().build() fileUploadInputDao.insert(it.copy(workerId = worker.id.toString())) fileUploadInputDao.delete(it) dashboardFileUploadDao.deleteByWorkerId(it.workerId) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModel.kt index 612dfae5a6..8b20af362f 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModel.kt @@ -632,7 +632,6 @@ class SpeedGraderCommentsViewModel @Inject constructor( private fun restartWorker(filePaths: List) { viewModelScope.launch { val worker = OneTimeWorkRequestBuilder() - .addTag(FileUploadWorker.WORKER_TAG) .build() val inputData = FileUploadInputEntity( From fae5e6c01d9c88d067714ececb7ee1d3d7744e9c Mon Sep 17 00:00:00 2001 From: "kristof.nemere" Date: Fri, 10 Oct 2025 18:26:03 +0200 Subject: [PATCH 23/23] test fix --- .../instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt | 2 ++ 1 file changed, 2 insertions(+) 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 818952275f..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 @@ -24,6 +24,7 @@ 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.annotations.Stub import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType @@ -44,6 +45,7 @@ class ShareExtensionE2ETest: StudentComposeTest() { override fun enableAndConfigureAccessibilityChecks() = Unit + @Stub @E2E @Test fun shareExtensionE2ETest() {