From 1144fdb060a72e3a3c741b5f4df489fa6a03fb04 Mon Sep 17 00:00:00 2001 From: lenovo Date: Thu, 12 Mar 2026 11:43:32 +0530 Subject: [PATCH 01/21] change history entity added migration added --- .../world/respect/app/app/AppNavHost.kt | 11 + .../apps/changehistory/ChangeHistoryScreen.kt | 39 + .../view/person/detail/PersonDetailScreen.kt | 11 + .../12.json | 1317 +++++++++++++++++ .../datalayer/db/RespectSchoolDatabase.kt | 9 +- .../db/RespectSchoolDatabaseMigrations.kt | 32 +- .../school/adapters/ChangeHistoryAdapter.kt | 40 + .../db/school/daos/ChangeHistoryDao.kt | 34 + .../db/school/entities/ChangeHistoryChange.kt | 11 + .../entities/ChangeHistoryChangeEntity.kt | 24 + .../db/school/entities/ChangeHistoryEntity.kt | 21 + .../entities/ChangeHistoryWithChanges.kt | 16 + .../school/model/ChangeHistoryEntry.kt | 19 + .../school/model/ChangeHistoryTableEnum.kt | 40 + .../composeResources/values/strings.xml | 1 + .../respect/shared/navigation/AppRoutes.kt | 5 + .../apps/changehistory/ChangeHistoryItem.kt | 49 + .../person/detail/PersonDetailViewModel.kt | 24 +- 18 files changed, 1695 insertions(+), 8 deletions(-) create mode 100644 respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt create mode 100644 respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/12.json create mode 100644 respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt create mode 100644 respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt create mode 100644 respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryChange.kt create mode 100644 respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryChangeEntity.kt create mode 100644 respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryEntity.kt create mode 100644 respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryWithChanges.kt create mode 100644 respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt create mode 100644 respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryTableEnum.kt create mode 100644 respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryItem.kt diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt index 59b3b4972..05afdbc9e 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt @@ -7,6 +7,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import world.respect.app.view.acknowledgement.AcknowledgementScreen +import world.respect.app.view.apps.changehistory.ChangeHistoryScreen import world.respect.app.view.apps.detail.AppsDetailScreen import world.respect.app.view.apps.enterlink.EnterLinkScreen import world.respect.app.view.apps.launcher.AppLauncherScreen @@ -72,6 +73,7 @@ import world.respect.shared.navigation.ClazzDetail import world.respect.shared.navigation.ClazzEdit import world.respect.shared.navigation.ClazzList import world.respect.shared.navigation.AcceptInvite +import world.respect.shared.navigation.ChangeHistory import world.respect.shared.navigation.CopyCode import world.respect.shared.navigation.CreateAccount import world.respect.shared.navigation.CreateAccountSetPassword @@ -646,6 +648,15 @@ fun AppNavHost( ) ) } + + composable { + ChangeHistoryScreen( + viewModel = respectViewModel( + onSetAppUiState = onSetAppUiState, + navController = respectNavController, + ) + ) + } } } diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt new file mode 100644 index 000000000..e19d9cfff --- /dev/null +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt @@ -0,0 +1,39 @@ +package world.respect.app.view.apps.changehistory + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import world.respect.shared.viewmodel.apps.changehistory.ChangeHistoryUiState +import world.respect.shared.viewmodel.apps.changehistory.ChangeHistoryViewModel + +@Composable +fun ChangeHistoryScreen( + viewModel: ChangeHistoryViewModel, +) { + val uiState by viewModel.uiState.collectAsState() + + ChangeHistoryScreen( + uiState = uiState + ) + +} + +@Composable +fun ChangeHistoryScreen( + uiState: ChangeHistoryUiState, +) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + + + } + +} \ No newline at end of file diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/person/detail/PersonDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/person/detail/PersonDetailScreen.kt index a55a55cfc..256135a91 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/person/detail/PersonDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/person/detail/PersonDetailScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Key +import androidx.compose.material.icons.filled.Replay import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme @@ -30,6 +31,7 @@ import world.respect.shared.generated.resources.email import world.respect.shared.generated.resources.family_members import world.respect.shared.generated.resources.gender import world.respect.shared.generated.resources.manage_account +import world.respect.shared.generated.resources.change_history import world.respect.shared.generated.resources.phone_number import world.respect.shared.generated.resources.role import world.respect.shared.generated.resources.username_label @@ -47,6 +49,7 @@ fun PersonDetailScreen( onClickManageAccount = viewModel::navigateToManageAccount, onClickCreateAccount = viewModel::onClickCreateAccount, onClickPhoneNumber = viewModel::onClickPhoneNumber, + onClickChangeHistoryButton = viewModel::onClickChangeHistoryButton, onClickFamilyMember = viewModel::onClickFamilyMember ) } @@ -56,6 +59,7 @@ fun PersonDetailScreen( uiState: PersonDetailUiState, onClickManageAccount:() -> Unit, onClickCreateAccount: () -> Unit, + onClickChangeHistoryButton: () -> Unit, onClickPhoneNumber: () -> Unit, onClickFamilyMember: (String) -> Unit, ) { @@ -80,6 +84,13 @@ fun PersonDetailScreen( onClick = onClickCreateAccount, ) } + if(uiState.changeHistoryButtonVisible){ + RespectQuickActionButton( + labelText = stringResource(Res.string.change_history), + imageVector = Icons.Default.Replay, + onClick = onClickChangeHistoryButton, + ) + } } HorizontalDivider() diff --git a/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/12.json b/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/12.json new file mode 100644 index 000000000..4ea7dfc1a --- /dev/null +++ b/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/12.json @@ -0,0 +1,1317 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "a1d2a8d27768ea0e89527afe85c4f600", + "entities": [ + { + "tableName": "SchoolAppEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`saUid` TEXT NOT NULL, `saUidNum` INTEGER NOT NULL, `saManifestUrl` TEXT NOT NULL, `saStatus` INTEGER NOT NULL, `saLastModified` INTEGER NOT NULL, `saStored` INTEGER NOT NULL, PRIMARY KEY(`saUidNum`))", + "fields": [ + { + "fieldPath": "saUid", + "columnName": "saUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "saUidNum", + "columnName": "saUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "saManifestUrl", + "columnName": "saManifestUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "saStatus", + "columnName": "saStatus", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "saLastModified", + "columnName": "saLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "saStored", + "columnName": "saStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "saUidNum" + ] + } + }, + { + "tableName": "PersonEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pGuid` TEXT NOT NULL, `pGuidHash` INTEGER NOT NULL, `pActive` INTEGER NOT NULL, `pStatus` INTEGER NOT NULL, `pLastModified` INTEGER NOT NULL, `pStored` INTEGER NOT NULL, `pMetadata` TEXT, `pUsername` TEXT, `pGivenName` TEXT NOT NULL, `pFamilyName` TEXT NOT NULL, `pMiddleName` TEXT, `pGender` INTEGER NOT NULL, `pDateOfBirth` INTEGER, `pEmail` TEXT, `pPhoneNumber` TEXT, PRIMARY KEY(`pGuidHash`))", + "fields": [ + { + "fieldPath": "pGuid", + "columnName": "pGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pGuidHash", + "columnName": "pGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pActive", + "columnName": "pActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pStatus", + "columnName": "pStatus", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pLastModified", + "columnName": "pLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pStored", + "columnName": "pStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pMetadata", + "columnName": "pMetadata", + "affinity": "TEXT" + }, + { + "fieldPath": "pUsername", + "columnName": "pUsername", + "affinity": "TEXT" + }, + { + "fieldPath": "pGivenName", + "columnName": "pGivenName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pFamilyName", + "columnName": "pFamilyName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pMiddleName", + "columnName": "pMiddleName", + "affinity": "TEXT" + }, + { + "fieldPath": "pGender", + "columnName": "pGender", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pDateOfBirth", + "columnName": "pDateOfBirth", + "affinity": "INTEGER" + }, + { + "fieldPath": "pEmail", + "columnName": "pEmail", + "affinity": "TEXT" + }, + { + "fieldPath": "pPhoneNumber", + "columnName": "pPhoneNumber", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "pGuidHash" + ] + } + }, + { + "tableName": "PersonRoleEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`prUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `prPersonGuidHash` INTEGER NOT NULL, `prIsPrimaryRole` INTEGER NOT NULL, `prRoleEnum` INTEGER NOT NULL, `prBeginDate` INTEGER, `prEndDate` INTEGER)", + "fields": [ + { + "fieldPath": "prUid", + "columnName": "prUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prPersonGuidHash", + "columnName": "prPersonGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prIsPrimaryRole", + "columnName": "prIsPrimaryRole", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prRoleEnum", + "columnName": "prRoleEnum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prBeginDate", + "columnName": "prBeginDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "prEndDate", + "columnName": "prEndDate", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "prUid" + ] + } + }, + { + "tableName": "PersonRelatedPersonEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`prpUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `prpPersonUidNum` INTEGER NOT NULL, `prpOtherPersonUid` TEXT NOT NULL, `prpOtherPersonUidNum` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "prpUid", + "columnName": "prpUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prpPersonUidNum", + "columnName": "prpPersonUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prpOtherPersonUid", + "columnName": "prpOtherPersonUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "prpOtherPersonUidNum", + "columnName": "prpOtherPersonUidNum", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "prpUid" + ] + } + }, + { + "tableName": "PersonPasswordEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ppwGuidNum` INTEGER NOT NULL, `ppwGuid` TEXT NOT NULL, `authAlgorithm` TEXT NOT NULL, `authEncoded` TEXT NOT NULL, `authSalt` TEXT NOT NULL, `authIterations` INTEGER NOT NULL, `authKeyLen` INTEGER NOT NULL, `ppwLastModified` INTEGER NOT NULL, `ppwStored` INTEGER NOT NULL, PRIMARY KEY(`ppwGuidNum`))", + "fields": [ + { + "fieldPath": "ppwGuidNum", + "columnName": "ppwGuidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppwGuid", + "columnName": "ppwGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authAlgorithm", + "columnName": "authAlgorithm", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authEncoded", + "columnName": "authEncoded", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authSalt", + "columnName": "authSalt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authIterations", + "columnName": "authIterations", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authKeyLen", + "columnName": "authKeyLen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppwLastModified", + "columnName": "ppwLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppwStored", + "columnName": "ppwStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "ppwGuidNum" + ] + } + }, + { + "tableName": "PersonPasskeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ppPersonUidNum` INTEGER NOT NULL, `ppCredentialId` TEXT NOT NULL, `ppLastModified` INTEGER NOT NULL, `ppStored` INTEGER NOT NULL, `ppAttestationObj` TEXT, `ppClientDataJson` TEXT, `ppOriginString` TEXT, `ppChallengeString` TEXT, `ppPublicKey` TEXT, `isRevoked` INTEGER NOT NULL, `ppDeviceName` TEXT NOT NULL DEFAULT '', `ppTimeCreated` INTEGER NOT NULL DEFAULT 0, `ppAaguid` TEXT NOT NULL DEFAULT '', `ppProviderName` TEXT NOT NULL DEFAULT '', `ppIconLight` TEXT NOT NULL DEFAULT '', `ppIconDark` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`ppPersonUidNum`, `ppCredentialId`))", + "fields": [ + { + "fieldPath": "ppPersonUidNum", + "columnName": "ppPersonUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppCredentialId", + "columnName": "ppCredentialId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ppLastModified", + "columnName": "ppLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppStored", + "columnName": "ppStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppAttestationObj", + "columnName": "ppAttestationObj", + "affinity": "TEXT" + }, + { + "fieldPath": "ppClientDataJson", + "columnName": "ppClientDataJson", + "affinity": "TEXT" + }, + { + "fieldPath": "ppOriginString", + "columnName": "ppOriginString", + "affinity": "TEXT" + }, + { + "fieldPath": "ppChallengeString", + "columnName": "ppChallengeString", + "affinity": "TEXT" + }, + { + "fieldPath": "ppPublicKey", + "columnName": "ppPublicKey", + "affinity": "TEXT" + }, + { + "fieldPath": "isRevoked", + "columnName": "isRevoked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppDeviceName", + "columnName": "ppDeviceName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "ppTimeCreated", + "columnName": "ppTimeCreated", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ppAaguid", + "columnName": "ppAaguid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "ppProviderName", + "columnName": "ppProviderName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "ppIconLight", + "columnName": "ppIconLight", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "ppIconDark", + "columnName": "ppIconDark", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "ppPersonUidNum", + "ppCredentialId" + ] + } + }, + { + "tableName": "AuthTokenEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`atUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `atPGuidHash` INTEGER NOT NULL, `atPGuid` TEXT NOT NULL, `atCode` TEXT, `atToken` TEXT NOT NULL, `atTimeCreated` INTEGER NOT NULL, `atTtl` INTEGER NOT NULL, `atPlatform` TEXT, `atAndroidSdkInt` INTEGER, `atVersion` TEXT, `atManufacturer` TEXT, `atModel` TEXT, `atRam` INTEGER)", + "fields": [ + { + "fieldPath": "atUid", + "columnName": "atUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "atPGuidHash", + "columnName": "atPGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "atPGuid", + "columnName": "atPGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "atCode", + "columnName": "atCode", + "affinity": "TEXT" + }, + { + "fieldPath": "atToken", + "columnName": "atToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "atTimeCreated", + "columnName": "atTimeCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "atTtl", + "columnName": "atTtl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "atPlatform", + "columnName": "atPlatform", + "affinity": "TEXT" + }, + { + "fieldPath": "atAndroidSdkInt", + "columnName": "atAndroidSdkInt", + "affinity": "INTEGER" + }, + { + "fieldPath": "atVersion", + "columnName": "atVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "atManufacturer", + "columnName": "atManufacturer", + "affinity": "TEXT" + }, + { + "fieldPath": "atModel", + "columnName": "atModel", + "affinity": "TEXT" + }, + { + "fieldPath": "atRam", + "columnName": "atRam", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "atUid" + ] + } + }, + { + "tableName": "ReportEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rGuid` TEXT NOT NULL, `rOwnerGuid` TEXT NOT NULL, `rTitle` TEXT NOT NULL, `rOptions` TEXT NOT NULL, `rIsTemplate` INTEGER NOT NULL, `rActive` INTEGER NOT NULL, `rLastModified` INTEGER NOT NULL, `rStored` INTEGER NOT NULL, PRIMARY KEY(`rGuid`))", + "fields": [ + { + "fieldPath": "rGuid", + "columnName": "rGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rOwnerGuid", + "columnName": "rOwnerGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rTitle", + "columnName": "rTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rOptions", + "columnName": "rOptions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rIsTemplate", + "columnName": "rIsTemplate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rActive", + "columnName": "rActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rLastModified", + "columnName": "rLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rStored", + "columnName": "rStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "rGuid" + ] + } + }, + { + "tableName": "IndicatorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`indicatorId` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, `type` TEXT NOT NULL, `sql` TEXT NOT NULL, PRIMARY KEY(`indicatorId`))", + "fields": [ + { + "fieldPath": "indicatorId", + "columnName": "indicatorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sql", + "columnName": "sql", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "indicatorId" + ] + } + }, + { + "tableName": "ClassEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cGuid` TEXT NOT NULL, `cGuidHash` INTEGER NOT NULL, `cTitle` TEXT NOT NULL, `cStatus` INTEGER NOT NULL, `cDescription` TEXT, `cLastModified` INTEGER NOT NULL, `cStored` INTEGER NOT NULL, `cTeacherInviteGuid` TEXT, `cStudentInviteGuid` TEXT, PRIMARY KEY(`cGuidHash`))", + "fields": [ + { + "fieldPath": "cGuid", + "columnName": "cGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cGuidHash", + "columnName": "cGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cTitle", + "columnName": "cTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cStatus", + "columnName": "cStatus", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cDescription", + "columnName": "cDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "cLastModified", + "columnName": "cLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cStored", + "columnName": "cStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cTeacherInviteGuid", + "columnName": "cTeacherInviteGuid", + "affinity": "TEXT" + }, + { + "fieldPath": "cStudentInviteGuid", + "columnName": "cStudentInviteGuid", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "cGuidHash" + ] + } + }, + { + "tableName": "ClassPermissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cpeId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `cpeClassUidNum` INTEGER NOT NULL, `cpeToEnrollmentRole` INTEGER, `cpePermissions` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "cpeId", + "columnName": "cpeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cpeClassUidNum", + "columnName": "cpeClassUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cpeToEnrollmentRole", + "columnName": "cpeToEnrollmentRole", + "affinity": "INTEGER" + }, + { + "fieldPath": "cpePermissions", + "columnName": "cpePermissions", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "cpeId" + ] + } + }, + { + "tableName": "EnrollmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eUid` TEXT NOT NULL, `eUidNum` INTEGER NOT NULL, `eStatus` INTEGER NOT NULL, `eLastModified` INTEGER NOT NULL, `eStored` INTEGER NOT NULL, `eMetadata` TEXT, `eClassUid` TEXT NOT NULL, `eClassUidNum` INTEGER NOT NULL, `ePersonUid` TEXT NOT NULL, `ePersonUidNum` INTEGER NOT NULL, `eRole` INTEGER NOT NULL, `eBeginDate` INTEGER, `eEndDate` INTEGER, `eRemovedAt` INTEGER, `eInviteCode` TEXT, `eApprovedByPersonUidNum` INTEGER NOT NULL, `eApprovedByPersonUid` TEXT, PRIMARY KEY(`eUidNum`))", + "fields": [ + { + "fieldPath": "eUid", + "columnName": "eUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eUidNum", + "columnName": "eUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eStatus", + "columnName": "eStatus", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eLastModified", + "columnName": "eLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eStored", + "columnName": "eStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eMetadata", + "columnName": "eMetadata", + "affinity": "TEXT" + }, + { + "fieldPath": "eClassUid", + "columnName": "eClassUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eClassUidNum", + "columnName": "eClassUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ePersonUid", + "columnName": "ePersonUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ePersonUidNum", + "columnName": "ePersonUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eRole", + "columnName": "eRole", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eBeginDate", + "columnName": "eBeginDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "eEndDate", + "columnName": "eEndDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "eRemovedAt", + "columnName": "eRemovedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "eInviteCode", + "columnName": "eInviteCode", + "affinity": "TEXT" + }, + { + "fieldPath": "eApprovedByPersonUidNum", + "columnName": "eApprovedByPersonUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eApprovedByPersonUid", + "columnName": "eApprovedByPersonUid", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "eUidNum" + ] + } + }, + { + "tableName": "AssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`aeUid` TEXT NOT NULL, `aeUidNum` INTEGER NOT NULL, `aeTitle` TEXT NOT NULL, `aeDescription` TEXT NOT NULL, `aeClassUid` TEXT NOT NULL, `aeClassUidNum` INTEGER NOT NULL, `aeDeadline` INTEGER, `aeLastModified` INTEGER NOT NULL, `aeStored` INTEGER NOT NULL, PRIMARY KEY(`aeUidNum`))", + "fields": [ + { + "fieldPath": "aeUid", + "columnName": "aeUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "aeUidNum", + "columnName": "aeUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "aeTitle", + "columnName": "aeTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "aeDescription", + "columnName": "aeDescription", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "aeClassUid", + "columnName": "aeClassUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "aeClassUidNum", + "columnName": "aeClassUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "aeDeadline", + "columnName": "aeDeadline", + "affinity": "INTEGER" + }, + { + "fieldPath": "aeLastModified", + "columnName": "aeLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "aeStored", + "columnName": "aeStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "aeUidNum" + ] + } + }, + { + "tableName": "AssignmentLearningResourceRefEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`alrrAeUidNum` INTEGER NOT NULL, `alrrLearningUnitManifestUrlHash` INTEGER NOT NULL, `alrrLearningUnitManifestUrl` TEXT NOT NULL, `alrrAppManifestUrl` TEXT NOT NULL, PRIMARY KEY(`alrrAeUidNum`, `alrrLearningUnitManifestUrlHash`))", + "fields": [ + { + "fieldPath": "alrrAeUidNum", + "columnName": "alrrAeUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alrrLearningUnitManifestUrlHash", + "columnName": "alrrLearningUnitManifestUrlHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alrrLearningUnitManifestUrl", + "columnName": "alrrLearningUnitManifestUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alrrAppManifestUrl", + "columnName": "alrrAppManifestUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "alrrAeUidNum", + "alrrLearningUnitManifestUrlHash" + ] + } + }, + { + "tableName": "WriteQueueItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`wqiQueueItemId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `wqiModel` INTEGER NOT NULL, `wqiUid` TEXT NOT NULL, `wqiTimeQueued` INTEGER NOT NULL, `wqiAttemptCount` INTEGER NOT NULL, `wqiTimeWritten` INTEGER NOT NULL, `wqiAccountGuid` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "wqiQueueItemId", + "columnName": "wqiQueueItemId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wqiModel", + "columnName": "wqiModel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wqiUid", + "columnName": "wqiUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wqiTimeQueued", + "columnName": "wqiTimeQueued", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wqiAttemptCount", + "columnName": "wqiAttemptCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wqiTimeWritten", + "columnName": "wqiTimeWritten", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wqiAccountGuid", + "columnName": "wqiAccountGuid", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "wqiQueueItemId" + ] + } + }, + { + "tableName": "SchoolPermissionGrantEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`spgUid` TEXT NOT NULL, `spgUidNum` INTEGER NOT NULL, `spgStatusEnum` INTEGER NOT NULL, `spgToRole` INTEGER NOT NULL, `spgPermissions` INTEGER NOT NULL, `spgLastModified` INTEGER NOT NULL, `spgStored` INTEGER NOT NULL, PRIMARY KEY(`spgUidNum`))", + "fields": [ + { + "fieldPath": "spgUid", + "columnName": "spgUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "spgUidNum", + "columnName": "spgUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spgStatusEnum", + "columnName": "spgStatusEnum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spgToRole", + "columnName": "spgToRole", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spgPermissions", + "columnName": "spgPermissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spgLastModified", + "columnName": "spgLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spgStored", + "columnName": "spgStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "spgUidNum" + ] + } + }, + { + "tableName": "PullSyncStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pssAccountPersonUid` TEXT NOT NULL, `pssAccountPersonUidNum` INTEGER NOT NULL, `pssTableId` INTEGER NOT NULL, `pssLastConsistentThrough` INTEGER NOT NULL, `pssPermissionsLastModified` INTEGER NOT NULL, PRIMARY KEY(`pssAccountPersonUid`, `pssTableId`))", + "fields": [ + { + "fieldPath": "pssAccountPersonUid", + "columnName": "pssAccountPersonUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pssAccountPersonUidNum", + "columnName": "pssAccountPersonUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pssTableId", + "columnName": "pssTableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pssLastConsistentThrough", + "columnName": "pssLastConsistentThrough", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pssPermissionsLastModified", + "columnName": "pssPermissionsLastModified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "pssAccountPersonUid", + "pssTableId" + ] + } + }, + { + "tableName": "PersonQrBadgeEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pqrGuidNum` INTEGER NOT NULL, `pqrGuid` TEXT NOT NULL, `pqrLastModified` INTEGER NOT NULL, `pqrStored` INTEGER NOT NULL, `pqrQrCodeUrl` TEXT, `pqrStatus` INTEGER NOT NULL, PRIMARY KEY(`pqrGuidNum`))", + "fields": [ + { + "fieldPath": "pqrGuidNum", + "columnName": "pqrGuidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pqrGuid", + "columnName": "pqrGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pqrLastModified", + "columnName": "pqrLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pqrStored", + "columnName": "pqrStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pqrQrCodeUrl", + "columnName": "pqrQrCodeUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "pqrStatus", + "columnName": "pqrStatus", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "pqrGuidNum" + ] + }, + "indices": [ + { + "name": "index_PersonQrBadgeEntity_pqrQrCodeUrl", + "unique": false, + "columnNames": [ + "pqrQrCodeUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PersonQrBadgeEntity_pqrQrCodeUrl` ON `${TABLE_NAME}` (`pqrQrCodeUrl`)" + } + ] + }, + { + "tableName": "InviteEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`iGuid` TEXT NOT NULL, `iGuidHash` INTEGER NOT NULL, `iCode` TEXT NOT NULL, `iApprovalRequiredAfter` INTEGER NOT NULL, `iLastModified` INTEGER NOT NULL, `iStored` INTEGER NOT NULL, `iStatus` INTEGER NOT NULL, `iNewUserRole` INTEGER, `iNewUserFirstInvite` INTEGER NOT NULL, `iForFamilyOfGuid` TEXT, `iForFamilyOfGuidHash` INTEGER, `iForClassGuid` TEXT, `iForClassName` TEXT, `iInviteMode` INTEGER, `iSchoolName` TEXT, `iForClassGuidHash` INTEGER, `iForClassRole` INTEGER, PRIMARY KEY(`iGuidHash`))", + "fields": [ + { + "fieldPath": "iGuid", + "columnName": "iGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iGuidHash", + "columnName": "iGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iCode", + "columnName": "iCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iApprovalRequiredAfter", + "columnName": "iApprovalRequiredAfter", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iLastModified", + "columnName": "iLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iStored", + "columnName": "iStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iStatus", + "columnName": "iStatus", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iNewUserRole", + "columnName": "iNewUserRole", + "affinity": "INTEGER" + }, + { + "fieldPath": "iNewUserFirstInvite", + "columnName": "iNewUserFirstInvite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iForFamilyOfGuid", + "columnName": "iForFamilyOfGuid", + "affinity": "TEXT" + }, + { + "fieldPath": "iForFamilyOfGuidHash", + "columnName": "iForFamilyOfGuidHash", + "affinity": "INTEGER" + }, + { + "fieldPath": "iForClassGuid", + "columnName": "iForClassGuid", + "affinity": "TEXT" + }, + { + "fieldPath": "iForClassName", + "columnName": "iForClassName", + "affinity": "TEXT" + }, + { + "fieldPath": "iInviteMode", + "columnName": "iInviteMode", + "affinity": "INTEGER" + }, + { + "fieldPath": "iSchoolName", + "columnName": "iSchoolName", + "affinity": "TEXT" + }, + { + "fieldPath": "iForClassGuidHash", + "columnName": "iForClassGuidHash", + "affinity": "INTEGER" + }, + { + "fieldPath": "iForClassRole", + "columnName": "iForClassRole", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "iGuidHash" + ] + } + }, + { + "tableName": "ChangeHistoryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hGuid` TEXT NOT NULL, `hGuidHash` INTEGER NOT NULL, `hTable` TEXT NOT NULL, `hTimestamp` INTEGER NOT NULL, `hWhoGuid` TEXT NOT NULL, `hWhoGuidHash` INTEGER NOT NULL, `hChanges` TEXT NOT NULL, PRIMARY KEY(`hGuidHash`))", + "fields": [ + { + "fieldPath": "hGuid", + "columnName": "hGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hGuidHash", + "columnName": "hGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hTable", + "columnName": "hTable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hTimestamp", + "columnName": "hTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hWhoGuid", + "columnName": "hWhoGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hWhoGuidHash", + "columnName": "hWhoGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hChanges", + "columnName": "hChanges", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "hGuidHash" + ] + } + }, + { + "tableName": "ChangeHistoryChangeEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hcId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `hcHistoryGuidHash` INTEGER NOT NULL, `hcField` TEXT NOT NULL, `hcOldVal` TEXT, `hcNewVal` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "hcId", + "columnName": "hcId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hcHistoryGuidHash", + "columnName": "hcHistoryGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hcField", + "columnName": "hcField", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hcOldVal", + "columnName": "hcOldVal", + "affinity": "TEXT" + }, + { + "fieldPath": "hcNewVal", + "columnName": "hcNewVal", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "hcId" + ] + } + } + ], + "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, 'a1d2a8d27768ea0e89527afe85c4f600')" + ] + } +} \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabase.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabase.kt index 2eb108dba..00ed912ee 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabase.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabase.kt @@ -21,6 +21,7 @@ import world.respect.datalayer.db.school.daos.ReportEntityDao import world.respect.datalayer.db.realm.entities.IndicatorEntity import world.respect.datalayer.db.school.daos.AssignmentEntityDao import world.respect.datalayer.db.school.daos.AssignmentLearningResourceRefEntityDao +import world.respect.datalayer.db.school.daos.ChangeHistoryDao import world.respect.datalayer.db.school.daos.ClassEntityDao import world.respect.datalayer.db.school.daos.ClassPermissionEntityDao import world.respect.datalayer.db.school.daos.EnrollmentEntityDao @@ -42,6 +43,8 @@ import world.respect.datalayer.db.school.entities.ReportEntity import world.respect.datalayer.db.school.entities.SchoolAppEntity import world.respect.datalayer.db.school.entities.WriteQueueItemEntity import world.respect.datalayer.db.school.daos.SchoolPermissionGrantDao +import world.respect.datalayer.db.school.entities.ChangeHistoryChangeEntity +import world.respect.datalayer.db.school.entities.ChangeHistoryEntity import world.respect.datalayer.db.school.entities.ClassPermissionEntity import world.respect.datalayer.db.school.entities.PullSyncStatusEntity import world.respect.datalayer.db.school.entities.SchoolPermissionGrantEntity @@ -71,8 +74,10 @@ import world.respect.datalayer.db.school.entities.SchoolPermissionGrantEntity PullSyncStatusEntity::class, PersonQrBadgeEntity::class, InviteEntity::class, + ChangeHistoryEntity::class, + ChangeHistoryChangeEntity::class, ], - version = 11, + version = 12, ) @TypeConverters(SharedConverters::class, SchoolTypeConverters::class) @ConstructedBy(RespectSchoolDatabaseConstructor::class) @@ -116,6 +121,8 @@ abstract class RespectSchoolDatabase: RoomDatabase() { abstract fun getPullSyncStatusEntityDao(): PullSyncStatusEntityDao + abstract fun getChangeHistoryDao(): ChangeHistoryDao + } // The Room compiler generates the `actual` implementations. diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabaseMigrations.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabaseMigrations.kt index 6826d39ba..19123abe2 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabaseMigrations.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabaseMigrations.kt @@ -112,12 +112,42 @@ val MIGRATION_7_8 = object : Migration(7, 8) { } } +val MIGRATE_11_12 = object : Migration(11, 12) { + + override fun migrate(connection: SQLiteConnection) { + + connection.execSQL(""" + CREATE TABLE IF NOT EXISTS `ChangeHistoryEntity` ( + `hGuid` TEXT NOT NULL, + `hGuidHash` INTEGER NOT NULL, + `hTable` TEXT NOT NULL, + `hTimestamp` INTEGER NOT NULL, + `hWhoGuid` TEXT NOT NULL, + `hWhoGuidHash` INTEGER NOT NULL, + `hChanges` TEXT NOT NULL, + PRIMARY KEY(`hGuidHash`) + ) + """.trimIndent()) + + connection.execSQL(""" + CREATE TABLE IF NOT EXISTS `ChangeHistoryChangeEntity` ( + `hcId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + `hcHistoryGuidHash` INTEGER NOT NULL, + `hcField` TEXT NOT NULL, + `hcOldVal` TEXT, + `hcNewVal` TEXT NOT NULL + ) + """.trimIndent()) + } +} + fun RoomDatabase.Builder.addCommonMigrations( ): RoomDatabase.Builder { return this.addMigrations( - MIGRATION_1_2, MIGRATE_3_4, MIGRATE_4_5,MIGRATE_5_6, MIGRATE_6_7,MIGRATION_7_8 + MIGRATION_1_2, MIGRATE_3_4, MIGRATE_4_5,MIGRATE_5_6, MIGRATE_6_7,MIGRATION_7_8, + MIGRATE_11_12 ) } diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt new file mode 100644 index 000000000..1deb39685 --- /dev/null +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt @@ -0,0 +1,40 @@ +package world.respect.datalayer.db.school.adapters + +import kotlinx.serialization.json.Json +import world.respect.datalayer.UidNumberMapper +import world.respect.datalayer.db.school.entities.ChangeHistoryEntity +import world.respect.datalayer.school.model.ChangeHistoryEntry + + +data class ChangeHistoryEntities( + val changeHistoryEntity: ChangeHistoryEntity +) + +fun ChangeHistoryEntities.toModel(): ChangeHistoryEntry { + return ChangeHistoryEntry( + guid = changeHistoryEntity.hGuid, + table = changeHistoryEntity.hTable, + timestamp = changeHistoryEntity.hTimestamp, + whoGuid = changeHistoryEntity.hWhoGuid, + changes = Json.decodeFromString(changeHistoryEntity.hChanges) + ) +} + +fun ChangeHistoryEntry.toEntities( + uidNumberMapper: UidNumberMapper +): ChangeHistoryEntities { + + val hGuidHash = uidNumberMapper(guid) + + return ChangeHistoryEntities( + changeHistoryEntity = ChangeHistoryEntity( + hGuid = guid, + hGuidHash = hGuidHash, + hTable = table, + hTimestamp = timestamp, + hWhoGuid = whoGuid, + hWhoGuidHash = uidNumberMapper(whoGuid), + hChanges = Json.encodeToString(changes) + ) + ) +} \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt new file mode 100644 index 000000000..07f816a0b --- /dev/null +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt @@ -0,0 +1,34 @@ +package world.respect.datalayer.db.school.daos + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Transaction +import world.respect.datalayer.db.school.entities.ChangeHistoryChangeEntity +import world.respect.datalayer.db.school.entities.ChangeHistoryEntity + +@Dao +interface ChangeHistoryDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertHistory( + entity: ChangeHistoryEntity + ) + + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertChanges( + entities: List + ) + + @Transaction + suspend fun insertHistoryWithChanges( + history: ChangeHistoryEntity, + changes: List + ) { + insertHistory(history) + insertChanges(changes) + } + + +} \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryChange.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryChange.kt new file mode 100644 index 000000000..f79b01016 --- /dev/null +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryChange.kt @@ -0,0 +1,11 @@ +package world.respect.datalayer.db.school.entities + +import kotlinx.serialization.Serializable +import world.respect.datalayer.school.model.ChangeHistoryFieldEnum + +@Serializable +data class ChangeHistoryChange( + val field: ChangeHistoryFieldEnum, + val newVal: String, + val oldVal: String? +) \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryChangeEntity.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryChangeEntity.kt new file mode 100644 index 000000000..9cc9ba7f5 --- /dev/null +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryChangeEntity.kt @@ -0,0 +1,24 @@ +package world.respect.datalayer.db.school.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import world.respect.datalayer.school.model.ChangeHistoryFieldEnum + +@Entity +data class ChangeHistoryChangeEntity( + + @PrimaryKey(autoGenerate = true) + val hcId: Long = 0, + + val hcHistoryGuidHash: Long, + + val hcField: ChangeHistoryFieldEnum, + + val hcOldVal: String?, + + val hcNewVal: String +){ + companion object{ + const val CHILD_COLUMN = "hcHistoryGuidHash" + } +} \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryEntity.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryEntity.kt new file mode 100644 index 000000000..9f0c17369 --- /dev/null +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryEntity.kt @@ -0,0 +1,21 @@ +package world.respect.datalayer.db.school.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import world.respect.datalayer.school.model.ChangeHistoryTableEnum + +@Entity +data class ChangeHistoryEntity( + val hGuid: String, + @PrimaryKey + val hGuidHash: Long, + val hTable: ChangeHistoryTableEnum, + val hTimestamp: Long, + val hWhoGuid: String, + val hWhoGuidHash: Long, + val hChanges: String +){ + companion object{ + const val PARENT_COLUMN = "hGuidHash" + } +} \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryWithChanges.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryWithChanges.kt new file mode 100644 index 000000000..66f4e5fe4 --- /dev/null +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryWithChanges.kt @@ -0,0 +1,16 @@ +package world.respect.datalayer.db.school.entities + +import androidx.room.Embedded +import androidx.room.Relation + +data class ChangeHistoryWithChanges( + + @Embedded + val history: ChangeHistoryEntity, + + @Relation( + parentColumn = ChangeHistoryEntity.PARENT_COLUMN, + entityColumn = ChangeHistoryChangeEntity.CHILD_COLUMN + ) + val changes: List +) \ No newline at end of file diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt new file mode 100644 index 000000000..1d2e7f091 --- /dev/null +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt @@ -0,0 +1,19 @@ +package world.respect.datalayer.school.model + +import kotlinx.serialization.Serializable + +@Serializable +data class ChangeHistoryEntry( + val guid: String, + val table: ChangeHistoryTableEnum, + val timestamp: Long, + val whoGuid: String, + val changes: List +) + +@Serializable +data class ChangeHistoryChange( + val field: ChangeHistoryFieldEnum, + val newVal: String, + val oldVal: String? +) \ No newline at end of file diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryTableEnum.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryTableEnum.kt new file mode 100644 index 000000000..6d13fbe6b --- /dev/null +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryTableEnum.kt @@ -0,0 +1,40 @@ +package world.respect.datalayer.school.model + + +enum class ChangeHistoryTableEnum(val value: String) { + + PERSON("person"), + CLASS("class"), + ENROLLMENT("enrollment"); + + companion object { + fun fromValue(value: String) = + entries.first { it.value == value } + } +} +enum class ChangeHistoryFieldEnum(val value: String) { + + PERSON_GIVEN_NAME("pGivenName"), + PERSON_FAMILY_NAME("pFamilyName"), + PERSON_MIDDLE_NAME("pMiddleName"), + PERSON_USERNAME("pUsername"), + PERSON_GENDER("pGender"), + PERSON_EMAIL("pEmail"), + PERSON_PHONE_NUMBER("pPhoneNumber"), + PERSON_DATE_OF_BIRTH("pDateOfBirth"), + + CLASS_TITLE("cTitle"), + CLASS_DESCRIPTION("cDescription"), + CLASS_STATUS("cStatus"), + + + ENROLLMENT_ROLE("eRole"), + ENROLLMENT_BEGIN_DATE("eBeginDate"), + ENROLLMENT_END_DATE("eEndDate"), + ENROLLMENT_STATUS("eStatus"); + + companion object { + fun fromValue(value: String) = + entries.first { it.value == value } + } +} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index 52b9cb015..a219c3cb4 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -110,6 +110,7 @@ Email Phone number Create account + Change history Username QR Code Badge Manage account diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 17746057e..cc754ab53 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -863,3 +863,8 @@ data class QrCode( data class CopyCode( val inviteCode:String?=null ): RespectAppRoute + +@Serializable +data class ChangeHistory( + val guid: String, +): RespectAppRoute \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryItem.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryItem.kt new file mode 100644 index 000000000..647b9e1d1 --- /dev/null +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryItem.kt @@ -0,0 +1,49 @@ +package world.respect.shared.viewmodel.apps.changehistory + +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.toRoute +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import org.koin.core.component.KoinScopeComponent +import org.koin.core.scope.Scope +import world.respect.shared.domain.account.RespectAccountManager +import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.change_history +import world.respect.shared.navigation.ChangeHistory +import world.respect.shared.util.ext.asUiText +import world.respect.shared.viewmodel.RespectViewModel +import world.respect.shared.viewmodel.app.appstate.FabUiState + + +data class ChangeHistoryUiState( + val guid: String = "", +) + +class ChangeHistoryViewModel( + savedStateHandle: SavedStateHandle, + accountManager: RespectAccountManager +) : RespectViewModel(savedStateHandle), KoinScopeComponent { + + override val scope: Scope = accountManager.requireActiveAccountScope() + + private val route: ChangeHistory = savedStateHandle.toRoute() + + private val _uiState = MutableStateFlow( + ChangeHistoryUiState( + guid = route.guid + ) + ) + + val uiState = _uiState.asStateFlow() + + init { + _appUiState.update { prev -> + prev.copy( + title = Res.string.change_history.asUiText(), + fabState = FabUiState() + ) + } + } + +} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/detail/PersonDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/detail/PersonDetailViewModel.kt index 677581a42..e73fc081e 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/detail/PersonDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/detail/PersonDetailViewModel.kt @@ -15,33 +15,34 @@ import world.respect.datalayer.DataLoadParams import world.respect.datalayer.DataLoadState import world.respect.datalayer.DataLoadingState import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.db.school.ext.fullName +import world.respect.datalayer.db.school.ext.isAdmin +import world.respect.datalayer.db.school.ext.isAdminOrTeacher import world.respect.datalayer.ext.dataOrNull import world.respect.datalayer.school.PersonDataSource +import world.respect.datalayer.school.domain.CheckPersonPermissionUseCase import world.respect.datalayer.school.model.Person import world.respect.datalayer.shared.params.GetListCommonParams import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.phonenumber.OnClickPhoneNumUseCase import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.edit +import world.respect.shared.navigation.ChangeHistory +import world.respect.shared.navigation.CreateAccountSetUsername import world.respect.shared.navigation.ManageAccount import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.PersonDetail import world.respect.shared.navigation.PersonEdit -import world.respect.shared.navigation.CreateAccountSetUsername import world.respect.shared.util.ext.asUiText -import world.respect.datalayer.db.school.ext.fullName -import world.respect.datalayer.db.school.ext.isAdmin -import world.respect.datalayer.db.school.ext.isAdminOrTeacher -import world.respect.datalayer.school.domain.CheckPersonPermissionUseCase import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState -import kotlin.getValue data class PersonDetailUiState( val guid: String = "", val persons: DataLoadState> = DataLoadingState(), val manageAccountVisible: Boolean = false, val createAccountVisible: Boolean = false, + val changeHistoryButtonVisible: Boolean = false, ) { val person: Person? @@ -118,6 +119,7 @@ class PersonDetailViewModel( prev.copy( persons = persons, manageAccountVisible = hasAccountPermission && personVal?.username != null, + changeHistoryButtonVisible = activeAccount?.person?.isAdmin() == true, createAccountVisible = personVal != null && activeAccount?.person?.isAdminOrTeacher() == true && personVal.username == null, @@ -165,4 +167,14 @@ class PersonDetailViewModel( ) } + fun onClickChangeHistoryButton(){ + _navCommandFlow.tryEmit( + NavCommand.Navigate( + ChangeHistory( + guid = "" + ) + ) + ) + } + } \ No newline at end of file From a4d299c3b35735d68e54dd526972163cbefb0698 Mon Sep 17 00:00:00 2001 From: lenovo Date: Sun, 15 Mar 2026 11:45:12 +0530 Subject: [PATCH 02/21] data source added --- .../datalayer/db/SchoolDataSourceDb.kt | 6 + .../db/school/ChangeHistoryDataSourceDb.kt | 72 ++++++++++++ .../school/adapters/ChangeHistoryAdapter.kt | 61 ++++++++--- .../db/school/daos/ChangeHistoryDao.kt | 39 +++++++ .../db/school/entities/ChangeHistoryChange.kt | 11 -- .../datalayer/http/SchoolDataSourceHttp.kt | 12 ++ .../school/ChangeHistoryDataSourceHttp.kt | 103 ++++++++++++++++++ .../repository/SchoolDataSourceRepository.kt | 9 ++ .../ChangeHistoryDataSourceRepository.kt | 72 ++++++++++++ .../respect/datalayer/DataLayerParams.kt | 2 + .../respect/datalayer/SchoolDataSource.kt | 3 + .../datalayer/SchoolDataSourceLocal.kt | 3 + .../school/ChangeHistoryDataSource.kt | 59 ++++++++++ .../datalayer/school/ChangeHistoryLocal.kt | 6 + .../world/respect/server/Application.kt | 2 + .../server/routes/ChangeHistoryRoute.kt | 44 ++++++++ 16 files changed, 475 insertions(+), 29 deletions(-) create mode 100644 respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt delete mode 100644 respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryChange.kt create mode 100644 respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ChangeHistoryDataSourceHttp.kt create mode 100644 respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/ChangeHistoryDataSourceRepository.kt create mode 100644 respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/ChangeHistoryDataSource.kt create mode 100644 respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/ChangeHistoryLocal.kt create mode 100644 respect-server/src/main/kotlin/world/respect/server/routes/ChangeHistoryRoute.kt diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt index 385cfbad4..e35a473c3 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt @@ -4,6 +4,7 @@ import world.respect.datalayer.AuthenticatedUserPrincipalId import world.respect.datalayer.SchoolDataSourceLocal import world.respect.datalayer.UidNumberMapper import world.respect.datalayer.db.school.AssignmentDatasourceDb +import world.respect.datalayer.db.school.ChangeHistoryDataSourceDb import world.respect.datalayer.db.school.ClassDatasourceDb import world.respect.datalayer.db.school.EnrollmentDataSourceDb import world.respect.datalayer.db.school.GetAuthenticatedPersonUseCase @@ -17,6 +18,7 @@ import world.respect.datalayer.db.school.ReportDataSourceDb import world.respect.datalayer.db.school.SchoolAppDataSourceDb import world.respect.datalayer.db.school.SchoolPermissionGrantDataSourceDb import world.respect.datalayer.school.AssignmentDataSourceLocal +import world.respect.datalayer.school.ChangeHistoryLocal import world.respect.datalayer.school.ClassDataSourceLocal import world.respect.datalayer.school.EnrollmentDataSourceLocal import world.respect.datalayer.school.IndicatorDataSource @@ -105,4 +107,8 @@ class SchoolDataSourceDb( override val assignmentDataSource: AssignmentDataSourceLocal by lazy { AssignmentDatasourceDb(schoolDb, uidNumberMapper, authenticatedUser) } + + override val changeHistoryDataSource: ChangeHistoryLocal by lazy { + ChangeHistoryDataSourceDb(schoolDb) + } } \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt new file mode 100644 index 000000000..a510b73eb --- /dev/null +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt @@ -0,0 +1,72 @@ +package world.respect.datalayer.db.school + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import world.respect.datalayer.DataLoadParams +import world.respect.datalayer.DataLoadState +import world.respect.datalayer.DataReadyState +import world.respect.datalayer.NoDataLoadedState +import world.respect.datalayer.db.RespectSchoolDatabase +import world.respect.datalayer.db.school.adapters.toModel +import world.respect.datalayer.school.ChangeHistoryDataSource +import world.respect.datalayer.school.ChangeHistoryLocal +import world.respect.datalayer.school.model.ChangeHistoryEntry +import world.respect.datalayer.shared.paging.IPagingSourceFactory +import world.respect.datalayer.shared.paging.map + +class ChangeHistoryDataSourceDb( + private val schoolDb: RespectSchoolDatabase, + ) : ChangeHistoryLocal { + + + + override suspend fun findByGuid( + loadParams: DataLoadParams, + guid: String + ): DataLoadState { + return schoolDb.getChangeHistoryDao() + .findByGuid(guid)?.let { + DataReadyState(it.toModel()) + }?: NoDataLoadedState.notFound() + } + + override fun findByGuidAsFlow( + loadParams: DataLoadParams, + guid: String + ): Flow> { + return schoolDb.getChangeHistoryDao() + .findByGuidAsFlow(guid).map { + it?.let { + DataReadyState(it.toModel()) + } ?: NoDataLoadedState.notFound() + } + } + + override fun listAsPagingSource( + dataLoadParams: DataLoadParams, + getListParams: ChangeHistoryDataSource.GetListParams + ): + IPagingSourceFactory { + + return IPagingSourceFactory { + schoolDb.getChangeHistoryDao() + .listAsPagingSource().map { + it.toModel() + } + } + } + + override suspend fun store(list: List) { + } + + override suspend fun updateLocal( + list: List, + forceOverwrite: Boolean + ) { + } + + override suspend fun findByUidList(uids: List): List { + TODO("Not yet implemented") + } + +} \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt index 1deb39685..8352bab60 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt @@ -1,22 +1,35 @@ package world.respect.datalayer.db.school.adapters -import kotlinx.serialization.json.Json import world.respect.datalayer.UidNumberMapper +import world.respect.datalayer.db.school.entities.ChangeHistoryChangeEntity import world.respect.datalayer.db.school.entities.ChangeHistoryEntity +import world.respect.datalayer.db.school.entities.ChangeHistoryWithChanges +import world.respect.datalayer.school.model.ChangeHistoryChange import world.respect.datalayer.school.model.ChangeHistoryEntry data class ChangeHistoryEntities( - val changeHistoryEntity: ChangeHistoryEntity + val changeHistoryEntity: ChangeHistoryEntity, + val changeEntities: List ) -fun ChangeHistoryEntities.toModel(): ChangeHistoryEntry { + +fun ChangeHistoryWithChanges.toModel(): ChangeHistoryEntry { return ChangeHistoryEntry( - guid = changeHistoryEntity.hGuid, - table = changeHistoryEntity.hTable, - timestamp = changeHistoryEntity.hTimestamp, - whoGuid = changeHistoryEntity.hWhoGuid, - changes = Json.decodeFromString(changeHistoryEntity.hChanges) + guid = history.hGuid, + table = history.hTable, + timestamp = history.hTimestamp, + whoGuid = history.hWhoGuid, + changes = changes.map { it.toModel() } + ) +} + + +fun ChangeHistoryChangeEntity.toModel(): ChangeHistoryChange { + return ChangeHistoryChange( + field = hcField, + oldVal = hcOldVal, + newVal = hcNewVal ) } @@ -24,17 +37,29 @@ fun ChangeHistoryEntry.toEntities( uidNumberMapper: UidNumberMapper ): ChangeHistoryEntities { - val hGuidHash = uidNumberMapper(guid) + val guidHash = uidNumberMapper(guid) - return ChangeHistoryEntities( - changeHistoryEntity = ChangeHistoryEntity( - hGuid = guid, - hGuidHash = hGuidHash, - hTable = table, - hTimestamp = timestamp, - hWhoGuid = whoGuid, - hWhoGuidHash = uidNumberMapper(whoGuid), - hChanges = Json.encodeToString(changes) + val historyEntity = ChangeHistoryEntity( + hGuid = guid, + hGuidHash = guidHash, + hTable = table, + hTimestamp = timestamp, + hWhoGuid = whoGuid, + hWhoGuidHash = uidNumberMapper(whoGuid), + hChanges = "" + ) + + val changeEntities = changes.map { change -> + ChangeHistoryChangeEntity( + hcHistoryGuidHash = guidHash, + hcField = change.field, + hcOldVal = change.oldVal, + hcNewVal = change.newVal ) + } + + return ChangeHistoryEntities( + changeHistoryEntity = historyEntity, + changeEntities = changeEntities ) } \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt index 07f816a0b..a964e76d8 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt @@ -1,11 +1,16 @@ package world.respect.datalayer.db.school.daos +import androidx.paging.PagingSource import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy +import androidx.room.Query import androidx.room.Transaction +import kotlinx.coroutines.flow.Flow import world.respect.datalayer.db.school.entities.ChangeHistoryChangeEntity import world.respect.datalayer.db.school.entities.ChangeHistoryEntity +import world.respect.datalayer.db.school.entities.ChangeHistoryWithChanges +import world.respect.datalayer.school.model.ChangeHistoryTableEnum @Dao interface ChangeHistoryDao { @@ -30,5 +35,39 @@ interface ChangeHistoryDao { insertChanges(changes) } + @Query(""" + SELECT * + FROM ChangeHistoryEntity + WHERE (:table IS NULL OR hTable = :table) + AND (:whoGuidHash = 0 OR hWhoGuidHash = :whoGuidHash) + ORDER BY hTimestamp DESC + """) + fun listAsPagingSource( + table: ChangeHistoryTableEnum?, + whoGuidHash: Long, + ): PagingSource + + @Query(""" + SELECT * + FROM ChangeHistoryEntity + WHERE hGuid = :guid + """) + suspend fun findByGuid(guid: String): ChangeHistoryWithChanges? + + + @Query(""" + SELECT * + FROM ChangeHistoryEntity + WHERE hGuid = :guid + """) + fun findByGuidAsFlow(guid: String): Flow + + + @Query(""" + SELECT * + FROM ChangeHistoryEntity + ORDER BY hGuidHash DESC + """) + fun listAsPagingSource(): PagingSource } \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryChange.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryChange.kt deleted file mode 100644 index f79b01016..000000000 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryChange.kt +++ /dev/null @@ -1,11 +0,0 @@ -package world.respect.datalayer.db.school.entities - -import kotlinx.serialization.Serializable -import world.respect.datalayer.school.model.ChangeHistoryFieldEnum - -@Serializable -data class ChangeHistoryChange( - val field: ChangeHistoryFieldEnum, - val newVal: String, - val oldVal: String? -) \ No newline at end of file diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/SchoolDataSourceHttp.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/SchoolDataSourceHttp.kt index ccf34bfb4..edc51a042 100644 --- a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/SchoolDataSourceHttp.kt +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/SchoolDataSourceHttp.kt @@ -5,6 +5,7 @@ import io.ktor.http.Url import world.respect.datalayer.AuthTokenProvider import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.http.school.AssignmentDataSourceHttp +import world.respect.datalayer.http.school.ChangeHistoryDataSourceHttp import world.respect.datalayer.http.school.ClassDataSourceHttp import world.respect.datalayer.http.school.EnrollmentDataSourceHttp import world.respect.datalayer.http.school.InviteDataSourceHttp @@ -16,6 +17,7 @@ import world.respect.datalayer.http.school.SchoolAppDataSourceHttp import world.respect.datalayer.http.school.SchoolPermissionGrantDataSourceHttp import world.respect.datalayer.networkvalidation.ExtendedDataSourceValidationHelper import world.respect.datalayer.school.AssignmentDataSource +import world.respect.datalayer.school.ChangeHistoryDataSource import world.respect.datalayer.school.ClassDataSource import world.respect.datalayer.school.EnrollmentDataSource import world.respect.datalayer.school.IndicatorDataSource @@ -142,4 +144,14 @@ class SchoolDataSourceHttp( validationHelper = validationHelper, ) } + + override val changeHistoryDataSource: ChangeHistoryDataSource by lazy { + ChangeHistoryDataSourceHttp( + schoolUrl = schoolUrl, + schoolDirectoryEntryDataSource = schoolDirectoryEntryDataSource, + httpClient = httpClient, + tokenProvider = tokenProvider, + validationHelper = validationHelper, + ) + } } diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ChangeHistoryDataSourceHttp.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ChangeHistoryDataSourceHttp.kt new file mode 100644 index 000000000..1d0f47196 --- /dev/null +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ChangeHistoryDataSourceHttp.kt @@ -0,0 +1,103 @@ +package world.respect.datalayer.http.school + +import io.ktor.client.HttpClient +import io.ktor.http.URLBuilder +import io.ktor.http.Url +import io.ktor.util.reflect.typeInfo +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import world.respect.datalayer.AuthTokenProvider +import world.respect.datalayer.DataLayerParams +import world.respect.datalayer.DataLoadParams +import world.respect.datalayer.DataLoadState +import world.respect.datalayer.ext.firstOrNotLoaded +import world.respect.datalayer.ext.getAsDataLoadState +import world.respect.datalayer.ext.getDataLoadResultAsFlow +import world.respect.datalayer.ext.useTokenProvider +import world.respect.datalayer.ext.useValidationCacheControl +import world.respect.datalayer.http.ext.appendCommonListParams +import world.respect.datalayer.http.ext.appendIfNotNull +import world.respect.datalayer.http.ext.respectEndpointUrl +import world.respect.datalayer.http.shared.paging.OffsetLimitHttpPagingSource +import world.respect.datalayer.networkvalidation.ExtendedDataSourceValidationHelper +import world.respect.datalayer.school.ChangeHistoryDataSource +import world.respect.datalayer.school.model.ChangeHistoryEntry +import world.respect.datalayer.schooldirectory.SchoolDirectoryEntryDataSource +import world.respect.datalayer.shared.paging.IPagingSourceFactory +import world.respect.datalayer.shared.params.GetListCommonParams + +class ChangeHistoryDataSourceHttp( + override val schoolUrl: Url, + override val schoolDirectoryEntryDataSource: SchoolDirectoryEntryDataSource, + private val httpClient: HttpClient, + private val tokenProvider: AuthTokenProvider, + private val validationHelper: ExtendedDataSourceValidationHelper?, +) : ChangeHistoryDataSource, SchoolUrlBasedDataSource { + + private suspend fun ChangeHistoryDataSource.GetListParams.urlWithParams(): Url { + return URLBuilder(respectEndpointUrl(ChangeHistoryDataSource.ENDPOINT_NAME)) + .apply { + parameters.appendCommonListParams(common) + parameters.appendIfNotNull(DataLayerParams.FILTER_BY_TABLE, filterByTable?.value) + parameters.appendIfNotNull(DataLayerParams.FILTER_BY_WHO_GUID, filterByWhoGuid) + } + .build() + } + + override suspend fun findByGuid( + loadParams: DataLoadParams, + guid: String + ): DataLoadState { + return httpClient.getAsDataLoadState>( + ChangeHistoryDataSource.GetListParams( + common = GetListCommonParams(guid = guid) + ).urlWithParams() + ) { + useTokenProvider(tokenProvider) + useValidationCacheControl(validationHelper) + }.firstOrNotLoaded() + } + + override fun findByGuidAsFlow( + loadParams: DataLoadParams, + guid: String + ): Flow> { + return httpClient.getDataLoadResultAsFlow>( + urlFn = { + ChangeHistoryDataSource.GetListParams( + common = GetListCommonParams(guid = guid) + ).urlWithParams() + }, + dataLoadParams = loadParams, + ) { + useTokenProvider(tokenProvider) + useValidationCacheControl(validationHelper) + }.map { + it.firstOrNotLoaded() + } + } + + + override fun listAsPagingSource( + dataLoadParams: DataLoadParams, + getListParams: ChangeHistoryDataSource.GetListParams + ): IPagingSourceFactory { + return IPagingSourceFactory { + OffsetLimitHttpPagingSource( + baseUrlProvider = { getListParams.urlWithParams() }, + httpClient = httpClient, + validationHelper = validationHelper, + typeInfo = typeInfo>(), + requestBuilder = { + useTokenProvider(tokenProvider) + useValidationCacheControl(validationHelper) + }, + logPrefixExtra = { "ChangeHistoryDataSource params=$getListParams" } + ) + } + } + + override suspend fun store(list: List) { + + } +} diff --git a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/SchoolDataSourceRepository.kt b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/SchoolDataSourceRepository.kt index 78c66b581..5c47992de 100644 --- a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/SchoolDataSourceRepository.kt +++ b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/SchoolDataSourceRepository.kt @@ -4,6 +4,7 @@ import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.SchoolDataSourceLocal import world.respect.datalayer.networkvalidation.ExtendedDataSourceValidationHelper import world.respect.datalayer.repository.school.AssignmentDataSourceRepository +import world.respect.datalayer.repository.school.ChangeHistoryDataSourceRepository import world.respect.datalayer.repository.school.ClassDataSourceRepository import world.respect.datalayer.repository.school.EnrollmentDataSourceRepository import world.respect.datalayer.repository.school.PersonDataSourceRepository @@ -13,6 +14,7 @@ import world.respect.datalayer.repository.school.PersonPasswordDataSourceReposit import world.respect.datalayer.repository.school.PersonQrCodeBadgeDataSourceRepository import world.respect.datalayer.repository.school.SchoolAppDataSourceRepository import world.respect.datalayer.repository.school.SchoolPermissionGrantDataSourceRepository +import world.respect.datalayer.school.ChangeHistoryDataSource import world.respect.datalayer.school.IndicatorDataSource import world.respect.datalayer.school.PersonPasskeyDataSource import world.respect.datalayer.school.ReportDataSource @@ -119,4 +121,11 @@ class SchoolDataSourceRepository( validationHelper = validationHelper ) } + override val changeHistoryDataSource: ChangeHistoryDataSource by lazy { + ChangeHistoryDataSourceRepository( + local = local.changeHistoryDataSource, + remote = remote.changeHistoryDataSource, + validationHelper = validationHelper + ) + } } \ No newline at end of file diff --git a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/ChangeHistoryDataSourceRepository.kt b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/ChangeHistoryDataSourceRepository.kt new file mode 100644 index 000000000..65d0b49cf --- /dev/null +++ b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/ChangeHistoryDataSourceRepository.kt @@ -0,0 +1,72 @@ +package world.respect.datalayer.repository.school + + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onEach +import world.respect.datalayer.DataLoadParams +import world.respect.datalayer.DataLoadState +import world.respect.datalayer.ext.combineWithRemote +import world.respect.datalayer.ext.updateFromRemoteIfNeeded +import world.respect.datalayer.networkvalidation.ExtendedDataSourceValidationHelper +import world.respect.datalayer.repository.shared.paging.RepositoryPagingSourceFactory +import world.respect.datalayer.repository.shared.paging.loadAndUpdateLocal2 +import world.respect.datalayer.school.ChangeHistoryDataSource +import world.respect.datalayer.school.ChangeHistoryLocal +import world.respect.datalayer.school.model.ChangeHistoryEntry +import world.respect.datalayer.shared.RepositoryModelDataSource +import world.respect.datalayer.shared.paging.IPagingSourceFactory + +class ChangeHistoryDataSourceRepository( + override val local: ChangeHistoryLocal, + override val remote: ChangeHistoryDataSource, + private val validationHelper: ExtendedDataSourceValidationHelper, +) : ChangeHistoryDataSource, RepositoryModelDataSource { + + override suspend fun findByGuid( + loadParams: DataLoadParams, + guid: String + ): DataLoadState { + local.updateFromRemoteIfNeeded( + remoteLoad = remote.findByGuid(loadParams, guid), + validationHelper = validationHelper, + ) + + return local.findByGuid(loadParams, guid) + } + + override fun findByGuidAsFlow( + loadParams: DataLoadParams, + guid: String + ): Flow> { + return local.findByGuidAsFlow(loadParams, guid).combineWithRemote( + remoteFlow = remote.findByGuidAsFlow(loadParams, guid).onEach { + local.updateFromRemoteIfNeeded(it, validationHelper) + } + ) + } + + override fun listAsPagingSource( + dataLoadParams: DataLoadParams, + getListParams: ChangeHistoryDataSource.GetListParams + ): IPagingSourceFactory { + val remote = remote.listAsPagingSource( + dataLoadParams = dataLoadParams, + getListParams = getListParams.copy(common = getListParams.common.copy(includeDeleted = true)), + ).invoke() + + return RepositoryPagingSourceFactory( + local = local.listAsPagingSource(dataLoadParams, getListParams), + onRemoteLoad = { remoteLoadParams -> + remote.loadAndUpdateLocal2( + loadParams = remoteLoadParams, + onUpdateLocalFromRemote = local::updateLocal, + ) + }, + tag = { "ChangeHistoryDataSourceRepo(listParams=$getListParams)" } + ) + } + + + override suspend fun store(list: List) { + } +} diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/DataLayerParams.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/DataLayerParams.kt index 828361fb7..226fb786a 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/DataLayerParams.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/DataLayerParams.kt @@ -34,6 +34,8 @@ object DataLayerParams { const val INVITE_REQUIRED = "inviteRequired" const val INVITE_STATUS = "inviteStatus" + const val FILTER_BY_TABLE = "filterByTable" + const val FILTER_BY_WHO_GUID = "filterByWhoGuid" } \ No newline at end of file diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/SchoolDataSource.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/SchoolDataSource.kt index d87e8ae1a..03d3c4511 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/SchoolDataSource.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/SchoolDataSource.kt @@ -1,6 +1,7 @@ package world.respect.datalayer import world.respect.datalayer.school.AssignmentDataSource +import world.respect.datalayer.school.ChangeHistoryDataSource import world.respect.datalayer.school.ClassDataSource import world.respect.datalayer.school.EnrollmentDataSource import world.respect.datalayer.school.ReportDataSource @@ -45,4 +46,6 @@ interface SchoolDataSource { val inviteDataSource: InviteDataSource + val changeHistoryDataSource: ChangeHistoryDataSource + } \ No newline at end of file diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/SchoolDataSourceLocal.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/SchoolDataSourceLocal.kt index 83e91be6d..da60727bd 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/SchoolDataSourceLocal.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/SchoolDataSourceLocal.kt @@ -1,6 +1,7 @@ package world.respect.datalayer import world.respect.datalayer.school.AssignmentDataSourceLocal +import world.respect.datalayer.school.ChangeHistoryLocal import world.respect.datalayer.school.ClassDataSourceLocal import world.respect.datalayer.school.EnrollmentDataSourceLocal import world.respect.datalayer.school.InviteDataSourceLocal @@ -40,4 +41,6 @@ interface SchoolDataSourceLocal: SchoolDataSource { override val assignmentDataSource: AssignmentDataSourceLocal override val inviteDataSource: InviteDataSourceLocal + + override val changeHistoryDataSource: ChangeHistoryLocal } \ No newline at end of file diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/ChangeHistoryDataSource.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/ChangeHistoryDataSource.kt new file mode 100644 index 000000000..142c5dfd9 --- /dev/null +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/ChangeHistoryDataSource.kt @@ -0,0 +1,59 @@ +package world.respect.datalayer.school + +import io.ktor.util.StringValues +import kotlinx.coroutines.flow.Flow +import world.respect.datalayer.DataLayerParams +import world.respect.datalayer.DataLoadParams +import world.respect.datalayer.DataLoadState +import world.respect.datalayer.school.model.ChangeHistoryEntry +import world.respect.datalayer.school.model.ChangeHistoryTableEnum +import world.respect.datalayer.shared.WritableDataSource +import world.respect.datalayer.shared.paging.IPagingSourceFactory +import world.respect.datalayer.shared.params.GetListCommonParams + +interface ChangeHistoryDataSource : WritableDataSource { + data class GetListParams( + val common: GetListCommonParams = GetListCommonParams(), + val filterByTable: ChangeHistoryTableEnum? = null, + val filterByWhoGuid: String? = null, + val sinceTimestamp: Long? = null, + ) { + + companion object { + + fun fromParams(stringValues: StringValues): GetListParams { + return GetListParams( + common = GetListCommonParams.fromParams(stringValues), + + filterByTable = stringValues[DataLayerParams.FILTER_BY_TABLE]?.let { + ChangeHistoryTableEnum.fromValue(it) + }, + + filterByWhoGuid = stringValues[DataLayerParams.FILTER_BY_WHO_GUID], + ) + } + } + } + + suspend fun findByGuid( + loadParams: DataLoadParams, + guid: String + ): DataLoadState + + fun findByGuidAsFlow( + loadParams: DataLoadParams, + guid: String + ): Flow> + + fun listAsPagingSource( + dataLoadParams: DataLoadParams, + getListParams: GetListParams + ): IPagingSourceFactory + + companion object { + + const val ENDPOINT_NAME = "changehistory" + + + } +} \ No newline at end of file diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/ChangeHistoryLocal.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/ChangeHistoryLocal.kt new file mode 100644 index 000000000..fc51e021f --- /dev/null +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/ChangeHistoryLocal.kt @@ -0,0 +1,6 @@ +package world.respect.datalayer.school + +import world.respect.datalayer.school.model.ChangeHistoryEntry +import world.respect.datalayer.shared.LocalModelDataSource + +interface ChangeHistoryLocal: ChangeHistoryDataSource, LocalModelDataSource diff --git a/respect-server/src/main/kotlin/world/respect/server/Application.kt b/respect-server/src/main/kotlin/world/respect/server/Application.kt index 0fa8a175a..3c5b8a839 100644 --- a/respect-server/src/main/kotlin/world/respect/server/Application.kt +++ b/respect-server/src/main/kotlin/world/respect/server/Application.kt @@ -38,6 +38,7 @@ import world.respect.datalayer.respect.model.SchoolDirectoryEntry import world.respect.libutil.ext.RESPECT_SCHOOL_LINK_SEGMENT import world.respect.libutil.util.throwable.ExceptionWithHttpStatusCode import world.respect.server.logging.LogbackAntiLog +import world.respect.server.routes.ChangeHistoryRoute import world.respect.server.routes.passkey.GetAllActivePasskeysRoute import world.respect.server.routes.passkey.RevokePasskeyRoute import world.respect.server.routes.passkey.VerifySignInWithPasskeyRoute @@ -250,6 +251,7 @@ fun Application.module() { EnrollmentRoute() AssignmentRoute() PersonQrBadgeRoute() + ChangeHistoryRoute() AddChildAccountRoute( addChildAccountUseCase = { it.requireAccountScope().get() } ) diff --git a/respect-server/src/main/kotlin/world/respect/server/routes/ChangeHistoryRoute.kt b/respect-server/src/main/kotlin/world/respect/server/routes/ChangeHistoryRoute.kt new file mode 100644 index 000000000..37514fecc --- /dev/null +++ b/respect-server/src/main/kotlin/world/respect/server/routes/ChangeHistoryRoute.kt @@ -0,0 +1,44 @@ +package world.respect.server.routes + + +import io.ktor.http.HttpHeaders +import io.ktor.server.application.ApplicationCall +import io.ktor.server.response.header +import io.ktor.server.routing.Route +import io.ktor.server.routing.get +import world.respect.datalayer.DataLoadParams +import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.school.ChangeHistoryDataSource +import world.respect.server.util.ext.offsetLimitPagingLoadParams +import world.respect.server.util.ext.requireAccountScope +import world.respect.server.util.ext.respondOffsetLimitPaging + +@Suppress("FunctionName") +fun Route.ChangeHistoryRoute( + schoolDataSource: (ApplicationCall) -> SchoolDataSource = { call -> + call.requireAccountScope().get() + }, +) { + get(ChangeHistoryDataSource.ENDPOINT_NAME) { + + val schoolDataSource = schoolDataSource(call) + + call.response.header(HttpHeaders.Vary, HttpHeaders.Authorization) + + val getListParams = ChangeHistoryDataSource.GetListParams.fromParams( + call.request.queryParameters + ) + + val loadParams = call.request.queryParameters.offsetLimitPagingLoadParams() + + call.respondOffsetLimitPaging( + params = loadParams, + pagingSource = schoolDataSource.changeHistoryDataSource + .listAsPagingSource( + DataLoadParams(), + getListParams + ).invoke() + ) + } +} + From 631371f8f9e065cd3de787be5e7d3189182354b3 Mon Sep 17 00:00:00 2001 From: lenovo Date: Mon, 16 Mar 2026 16:33:42 +0530 Subject: [PATCH 03/21] commit --- .../datalayer/db/SchoolDataSourceDb.kt | 2 +- .../db/school/ChangeHistoryDataSourceDb.kt | 16 +++++++++ .../school/adapters/ChangeHistoryAdapter.kt | 33 +++++++++++++++++++ .../school/ChangeHistoryDataSourceHttp.kt | 12 ++++++- .../http/school/PersonDataSourceHttp.kt | 2 +- .../repository/SchoolDataSourceRepository.kt | 6 ++-- .../ChangeHistoryDataSourceRepository.kt | 15 +++++++++ .../DrainRemoteWriteQueueUseCase.kt | 4 +++ .../datalayer/school/model/findDifference.kt | 21 ++++++++++++ .../school/writequeue/WriteQueueItem.kt | 1 + 10 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/findDifference.kt diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt index e35a473c3..575c1548f 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt @@ -109,6 +109,6 @@ class SchoolDataSourceDb( } override val changeHistoryDataSource: ChangeHistoryLocal by lazy { - ChangeHistoryDataSourceDb(schoolDb) + ChangeHistoryDataSourceDb(schoolDb,uidNumberMapper) } } \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt index a510b73eb..7c1311589 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt @@ -1,12 +1,16 @@ package world.respect.datalayer.db.school +import androidx.room.Transactor +import androidx.room.useWriterConnection import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import world.respect.datalayer.DataLoadParams import world.respect.datalayer.DataLoadState import world.respect.datalayer.DataReadyState import world.respect.datalayer.NoDataLoadedState +import world.respect.datalayer.UidNumberMapper import world.respect.datalayer.db.RespectSchoolDatabase +import world.respect.datalayer.db.school.adapters.toEntities import world.respect.datalayer.db.school.adapters.toModel import world.respect.datalayer.school.ChangeHistoryDataSource import world.respect.datalayer.school.ChangeHistoryLocal @@ -16,6 +20,7 @@ import world.respect.datalayer.shared.paging.map class ChangeHistoryDataSourceDb( private val schoolDb: RespectSchoolDatabase, + private val uidNumberMapper: UidNumberMapper ) : ChangeHistoryLocal { @@ -57,6 +62,17 @@ class ChangeHistoryDataSourceDb( } override suspend fun store(list: List) { + if(list.isEmpty()) + return + + schoolDb.useWriterConnection { con -> + con.withTransaction(Transactor.SQLiteTransactionType.IMMEDIATE) { + list.forEach { + schoolDb.getChangeHistoryDao().insertHistory(it.toEntities(uidNumberMapper).changeHistoryEntity) + + } + } + } } override suspend fun updateLocal( diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt index 8352bab60..79a131bd8 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt @@ -6,6 +6,10 @@ import world.respect.datalayer.db.school.entities.ChangeHistoryEntity import world.respect.datalayer.db.school.entities.ChangeHistoryWithChanges import world.respect.datalayer.school.model.ChangeHistoryChange import world.respect.datalayer.school.model.ChangeHistoryEntry +import world.respect.datalayer.school.model.ChangeHistoryFieldEnum +import world.respect.datalayer.school.model.ChangeHistoryTableEnum +import world.respect.datalayer.school.model.Person +import world.respect.datalayer.school.model.findDifference data class ChangeHistoryEntities( @@ -62,4 +66,33 @@ fun ChangeHistoryEntry.toEntities( changeHistoryEntity = historyEntity, changeEntities = changeEntities ) +} +fun generatePersonChanges( + hGuid: String, + old: Person?, + new: Person, + whoGuid: String, + timestamp: Long +): ChangeHistoryEntry? { + + val changes = mutableListOf() + + findDifference(ChangeHistoryFieldEnum.PERSON_GIVEN_NAME, old?.givenName, new.givenName, changes) + findDifference(ChangeHistoryFieldEnum.PERSON_FAMILY_NAME, old?.familyName, new.familyName, changes) + findDifference(ChangeHistoryFieldEnum.PERSON_MIDDLE_NAME, old?.middleName, new.middleName, changes) + findDifference(ChangeHistoryFieldEnum.PERSON_USERNAME, old?.username, new.username, changes) + findDifference(ChangeHistoryFieldEnum.PERSON_GENDER, old?.gender, new.gender, changes) + findDifference(ChangeHistoryFieldEnum.PERSON_EMAIL, old?.email, new.email, changes) + findDifference(ChangeHistoryFieldEnum.PERSON_PHONE_NUMBER, old?.phoneNumber, new.phoneNumber, changes) + findDifference(ChangeHistoryFieldEnum.PERSON_DATE_OF_BIRTH, old?.dateOfBirth, new.dateOfBirth, changes) + + if (changes.isEmpty()) return null + + return ChangeHistoryEntry( + guid = hGuid, + table = ChangeHistoryTableEnum.PERSON, + timestamp = timestamp, + whoGuid = whoGuid, + changes = changes + ) } \ No newline at end of file diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ChangeHistoryDataSourceHttp.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ChangeHistoryDataSourceHttp.kt index 1d0f47196..ea5e0b60c 100644 --- a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ChangeHistoryDataSourceHttp.kt +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ChangeHistoryDataSourceHttp.kt @@ -1,8 +1,12 @@ package world.respect.datalayer.http.school import io.ktor.client.HttpClient +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType import io.ktor.http.URLBuilder import io.ktor.http.Url +import io.ktor.http.contentType import io.ktor.util.reflect.typeInfo import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -98,6 +102,12 @@ class ChangeHistoryDataSourceHttp( } override suspend fun store(list: List) { - + httpClient.post( + respectEndpointUrl(ChangeHistoryDataSource.ENDPOINT_NAME) + ) { + useTokenProvider(tokenProvider) + contentType(ContentType.Application.Json) + setBody(list) + } } } diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/PersonDataSourceHttp.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/PersonDataSourceHttp.kt index febbf3288..ce7c1d655 100644 --- a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/PersonDataSourceHttp.kt +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/PersonDataSourceHttp.kt @@ -19,8 +19,8 @@ import world.respect.datalayer.ext.getAsDataLoadState import world.respect.datalayer.ext.getDataLoadResultAsFlow import world.respect.datalayer.ext.useTokenProvider import world.respect.datalayer.ext.useValidationCacheControl -import world.respect.datalayer.http.ext.appendIfNotNull import world.respect.datalayer.http.ext.appendCommonListParams +import world.respect.datalayer.http.ext.appendIfNotNull import world.respect.datalayer.http.ext.respectEndpointUrl import world.respect.datalayer.http.shared.paging.OffsetLimitHttpPagingSource import world.respect.datalayer.networkvalidation.ExtendedDataSourceValidationHelper diff --git a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/SchoolDataSourceRepository.kt b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/SchoolDataSourceRepository.kt index 5c47992de..ac207cb85 100644 --- a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/SchoolDataSourceRepository.kt +++ b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/SchoolDataSourceRepository.kt @@ -14,7 +14,6 @@ import world.respect.datalayer.repository.school.PersonPasswordDataSourceReposit import world.respect.datalayer.repository.school.PersonQrCodeBadgeDataSourceRepository import world.respect.datalayer.repository.school.SchoolAppDataSourceRepository import world.respect.datalayer.repository.school.SchoolPermissionGrantDataSourceRepository -import world.respect.datalayer.school.ChangeHistoryDataSource import world.respect.datalayer.school.IndicatorDataSource import world.respect.datalayer.school.PersonPasskeyDataSource import world.respect.datalayer.school.ReportDataSource @@ -121,11 +120,12 @@ class SchoolDataSourceRepository( validationHelper = validationHelper ) } - override val changeHistoryDataSource: ChangeHistoryDataSource by lazy { + override val changeHistoryDataSource: ChangeHistoryDataSourceRepository by lazy { ChangeHistoryDataSourceRepository( local = local.changeHistoryDataSource, remote = remote.changeHistoryDataSource, - validationHelper = validationHelper + validationHelper = validationHelper, + remoteWriteQueue = remoteWriteQueue, ) } } \ No newline at end of file diff --git a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/ChangeHistoryDataSourceRepository.kt b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/ChangeHistoryDataSourceRepository.kt index 65d0b49cf..75c890b47 100644 --- a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/ChangeHistoryDataSourceRepository.kt +++ b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/ChangeHistoryDataSourceRepository.kt @@ -13,13 +13,17 @@ import world.respect.datalayer.repository.shared.paging.loadAndUpdateLocal2 import world.respect.datalayer.school.ChangeHistoryDataSource import world.respect.datalayer.school.ChangeHistoryLocal import world.respect.datalayer.school.model.ChangeHistoryEntry +import world.respect.datalayer.school.writequeue.RemoteWriteQueue +import world.respect.datalayer.school.writequeue.WriteQueueItem import world.respect.datalayer.shared.RepositoryModelDataSource import world.respect.datalayer.shared.paging.IPagingSourceFactory +import world.respect.libutil.util.time.systemTimeInMillis class ChangeHistoryDataSourceRepository( override val local: ChangeHistoryLocal, override val remote: ChangeHistoryDataSource, private val validationHelper: ExtendedDataSourceValidationHelper, + private val remoteWriteQueue: RemoteWriteQueue, ) : ChangeHistoryDataSource, RepositoryModelDataSource { override suspend fun findByGuid( @@ -68,5 +72,16 @@ class ChangeHistoryDataSourceRepository( override suspend fun store(list: List) { + local.store(list) + val timeNow = systemTimeInMillis() + remoteWriteQueue.add( + list.map { + WriteQueueItem( + model = WriteQueueItem.Model.CHANGE_HISTORY, + uid = it.guid, + timeQueued = timeNow, + ) + } + ) } } diff --git a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/writequeue/DrainRemoteWriteQueueUseCase.kt b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/writequeue/DrainRemoteWriteQueueUseCase.kt index f7f81aa1f..de5a5223e 100644 --- a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/writequeue/DrainRemoteWriteQueueUseCase.kt +++ b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/writequeue/DrainRemoteWriteQueueUseCase.kt @@ -64,6 +64,10 @@ class DrainRemoteWriteQueueUseCase( WriteQueueItem.Model.INVITE -> { repository.inviteDataSource.sendToRemote(listOf(item)) } + WriteQueueItem.Model.CHANGE_HISTORY -> { + repository.changeHistoryDataSource.sendToRemote(listOf(item)) + } + } } diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/findDifference.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/findDifference.kt new file mode 100644 index 000000000..4fee2a81d --- /dev/null +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/findDifference.kt @@ -0,0 +1,21 @@ +package world.respect.datalayer.school.model + +fun findDifference( + field: ChangeHistoryFieldEnum, + oldVal: T?, + newVal: T?, + changes: MutableList +) { + val oldString = oldVal?.toString() + val newString = newVal?.toString() + + if (oldString != newString) { + changes.add( + ChangeHistoryChange( + field = field, + oldVal = oldString, + newVal = newString ?: "" + ) + ) + } +} \ No newline at end of file diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/writequeue/WriteQueueItem.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/writequeue/WriteQueueItem.kt index fae8e43df..4176f1bd1 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/writequeue/WriteQueueItem.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/writequeue/WriteQueueItem.kt @@ -25,6 +25,7 @@ class WriteQueueItem( ASSIGNMENT(5), SCHOOL_APP(6), SCHOOL_PERMISSION_GRANT(7), PERSON_QRBADGE(8), INVITE(9), + CHANGE_HISTORY(10), ; From 72988d6c539109984a8c10e37e1f618746b996a3 Mon Sep 17 00:00:00 2001 From: pooja Date: Mon, 16 Mar 2026 15:37:58 +0400 Subject: [PATCH 04/21] Added test for change history --- ...vite_users_using_qr_code_or_link_test.yaml | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml index 94623f25a..75eae4fdb 100644 --- a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml +++ b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml @@ -101,7 +101,19 @@ onFlowComplete: text: "Apps" - assertVisible: "Apps" - assertVisible: "Assignments" -- assertVisible: "People" +- tapOn: "People" +- tapOn: "TeacherA User" +- tapOn: + id: "floating_action_button" # Edit button +- tapOn: "Gender*" +- tapOn: "Male" +- tapOn: "Save" +- tapOn: "Change History" +- assertVisible: + id: "app_title" + text: "Change History" +- assertVisible: "Person gender changed from “Female” to “Male”" + - runFlow: file: "subflows/admin_add_class.yaml" env: @@ -199,6 +211,13 @@ onFlowComplete: - assertVisible: "Pending requests.*" - assertVisible: "Student User.*" - tapOn: "Accept Invite" +- assertNotVisible: "Pending requests.*" +- tapOn: "Change History" +- assertVisible: + id: "app_title" + text: "Change History" +- assertVisible: "Join request approved for “Student User”" + # E) Teacher share invite link for parent device based signup - tapOn: "Add student" From 2518430446df8f751f41b9cf8aaf4758d26b8e09 Mon Sep 17 00:00:00 2001 From: pooja Date: Mon, 16 Mar 2026 15:39:33 +0400 Subject: [PATCH 05/21] Added test for change history --- .../flows/001_001_invite_users_using_qr_code_or_link_test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml index 75eae4fdb..394497e68 100644 --- a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml +++ b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml @@ -217,7 +217,7 @@ onFlowComplete: id: "app_title" text: "Change History" - assertVisible: "Join request approved for “Student User”" - +- assertVisible: "Teacher added: “TeacherA User”" # E) Teacher share invite link for parent device based signup - tapOn: "Add student" From 696925421ade84649e3a71f515652139a765b42e Mon Sep 17 00:00:00 2001 From: lenovo Date: Tue, 17 Mar 2026 11:57:31 +0530 Subject: [PATCH 06/21] commit --- .../kotlin/world/respect/AppKoinModule.kt | 2 ++ respect-datalayer-db/build.gradle.kts | 2 ++ .../datalayer/db/SchoolDataSourceDb.kt | 2 +- .../db/school/ChangeHistoryDataSourceDb.kt | 6 +++-- .../datalayer/db/school/PersonDataSourceDb.kt | 25 ++++++++++++++++++- 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt index d30a17c45..857c55c7b 100644 --- a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt +++ b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt @@ -236,6 +236,7 @@ import world.respect.shared.viewmodel.person.setusernameandpassword.CreateAccoun import world.respect.shared.viewmodel.person.setusernameandpassword.CreateAccountSetUserNameViewModel import world.respect.shared.viewmodel.schooldirectory.edit.SchoolDirectoryEditViewModel import world.respect.shared.viewmodel.schooldirectory.list.SchoolDirectoryListViewModel +import world.respect.shared.viewmodel.apps.changehistory.ChangeHistoryViewModel import world.respect.shared.domain.sharelink.LaunchSendEmailUseCase import world.respect.shared.domain.sharelink.LaunchShareLinkUseCase import world.respect.shared.domain.sharelink.LaunchSendSmsUseCase @@ -385,6 +386,7 @@ val appKoinModule = module { viewModelOf(::EnrollmentEditViewModel) viewModelOf(::InviteQrViewModel) viewModelOf(::CreateAccountSetPasswordViewModel) + viewModelOf(::ChangeHistoryViewModel) single { diff --git a/respect-datalayer-db/build.gradle.kts b/respect-datalayer-db/build.gradle.kts index 352a1d197..c376aae8c 100644 --- a/respect-datalayer-db/build.gradle.kts +++ b/respect-datalayer-db/build.gradle.kts @@ -35,6 +35,8 @@ kotlin { api(libs.androidx.paging.common) api(libs.androidx.room.paging) implementation(libs.napier) + implementation(libs.koin.core) + implementation(project.dependencies.platform(libs.koin.bom)) } diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt index 575c1548f..b0ddf0a62 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt @@ -68,7 +68,7 @@ class SchoolDataSourceDb( } override val personDataSource: PersonDataSourceLocal by lazy { - PersonDataSourceDb(schoolDb, uidNumberMapper, authenticatedUser, checkPersonPermissionUseCase) + PersonDataSourceDb(schoolDb, uidNumberMapper, authenticatedUser, checkPersonPermissionUseCase,changeHistoryDataSource) } override val personPasskeyDataSource: PersonPasskeyDataSourceLocal by lazy { diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt index 7c1311589..8ea16e80c 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt @@ -68,8 +68,10 @@ class ChangeHistoryDataSourceDb( schoolDb.useWriterConnection { con -> con.withTransaction(Transactor.SQLiteTransactionType.IMMEDIATE) { list.forEach { - schoolDb.getChangeHistoryDao().insertHistory(it.toEntities(uidNumberMapper).changeHistoryEntity) - + schoolDb.getChangeHistoryDao().insertHistoryWithChanges( + history = it.toEntities(uidNumberMapper).changeHistoryEntity, + changes = it.toEntities(uidNumberMapper).changeEntities + ) } } } diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/PersonDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/PersonDataSourceDb.kt index 5f7d33034..fbaf42121 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/PersonDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/PersonDataSourceDb.kt @@ -12,10 +12,12 @@ import world.respect.datalayer.DataReadyState import world.respect.datalayer.NoDataLoadedState import world.respect.datalayer.UidNumberMapper import world.respect.datalayer.db.RespectSchoolDatabase +import world.respect.datalayer.db.school.adapters.generatePersonChanges import world.respect.datalayer.db.school.adapters.toEntities import world.respect.datalayer.db.school.adapters.toModel import world.respect.datalayer.db.school.adapters.toPersonEntities import world.respect.datalayer.exceptions.ForbiddenException +import world.respect.datalayer.school.ChangeHistoryLocal import world.respect.datalayer.school.PersonDataSource import world.respect.datalayer.school.PersonDataSourceLocal import world.respect.datalayer.school.domain.CheckPersonPermissionUseCase @@ -27,6 +29,7 @@ import world.respect.datalayer.shared.maxLastStoredOrNull import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.map import world.respect.libutil.util.time.atStartOfDayInMillisUtc +import world.respect.libutil.util.time.systemTimeInMillis import kotlin.time.Clock class PersonDataSourceDb( @@ -34,7 +37,8 @@ class PersonDataSourceDb( private val uidNumberMapper: UidNumberMapper, private val authenticatedUser: AuthenticatedUserPrincipalId, private val checkPersonPermissionUseCase: CheckPersonPermissionUseCase, -): PersonDataSourceLocal { + private val changeHistoryDataSource: ChangeHistoryLocal, +): PersonDataSourceLocal { private suspend fun doUpsertPerson( @@ -87,7 +91,26 @@ class PersonDataSourceDb( ) { throw ForbiddenException("Authenticated user does not have permission to store ${personToStore.guid}") } + val timeNow = systemTimeInMillis() + val changeEntries = list.mapNotNull { person -> + + val oldPerson = schoolDb.getPersonEntityDao().findByGuidNum( + guidHash = uidNumberMapper(person.guid) + ) + + generatePersonChanges( + hGuid = person.guid, + old = oldPerson?.toPersonEntities()?.toModel(), + new = person, + whoGuid = "", + timestamp = timeNow + ) + } + + if (changeEntries.isNotEmpty()) { + changeHistoryDataSource.store(changeEntries) + } //Check that roles have not been change doUpsertPerson(personToStore) } From 6a9ca60a6f29ec5b053ddf1ee6c9251b7711e34e Mon Sep 17 00:00:00 2001 From: pooja Date: Tue, 17 Mar 2026 13:56:20 +0400 Subject: [PATCH 07/21] update test with a name change from "TeacherA" to "TeacherB". --- ...1_invite_users_using_qr_code_or_link_test.yaml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml index 394497e68..a603833fd 100644 --- a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml +++ b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml @@ -105,14 +105,21 @@ onFlowComplete: - tapOn: "TeacherA User" - tapOn: id: "floating_action_button" # Edit button -- tapOn: "Gender*" -- tapOn: "Male" +- tapOn: "First name*" +- runFlow: + file: "subflows/erase_text.yaml" + env: + TEXT: "First name*" +- inputText: "TeacherB" +- runScript: + file: "scripts/setDate.js" +- inputText: ${output.pastYearDateC} - tapOn: "Save" - tapOn: "Change History" - assertVisible: id: "app_title" text: "Change History" -- assertVisible: "Person gender changed from “Female” to “Male”" +- assertVisible: "Person name changed from “TeacherA User” to “TeacherB User”" - runFlow: file: "subflows/admin_add_class.yaml" @@ -217,7 +224,7 @@ onFlowComplete: id: "app_title" text: "Change History" - assertVisible: "Join request approved for “Student User”" -- assertVisible: "Teacher added: “TeacherA User”" +- assertVisible: "Teacher added: “TeacherB User”" # E) Teacher share invite link for parent device based signup - tapOn: "Add student" From b3632c3e6960c147500dcc6a57be3c57c476305f Mon Sep 17 00:00:00 2001 From: lenovo Date: Tue, 17 Mar 2026 18:55:12 +0530 Subject: [PATCH 08/21] commit --- .../apps/changehistory/ChangeHistoryScreen.kt | 42 ++++++- .../12.json | 18 +-- .../db/RespectSchoolDatabaseMigrations.kt | 2 +- .../db/school/ChangeHistoryDataSourceDb.kt | 12 +- .../datalayer/db/school/PersonDataSourceDb.kt | 28 ++--- .../school/adapters/ChangeHistoryAdapter.kt | 9 +- .../db/school/daos/ChangeHistoryDao.kt | 10 +- .../db/school/entities/ChangeHistoryEntity.kt | 2 +- .../school/ChangeHistoryDataSourceHttp.kt | 4 +- .../ChangeHistoryDataSourceRepository.kt | 5 +- .../school/ChangeHistoryDataSource.kt | 2 +- .../school/model/ChangeHistoryEntry.kt | 7 ++ .../school/model/ChangeHistoryTableEnum.kt | 50 +++++---- .../composeResources/values/strings.xml | 2 +- .../respect/shared/navigation/AppRoutes.kt | 22 +++- .../apps/changehistory/ChangeHistoryItem.kt | 49 --------- .../changehistory/ChangeHistoryViewModel.kt | 104 ++++++++++++++++++ .../person/detail/PersonDetailViewModel.kt | 4 +- 18 files changed, 256 insertions(+), 116 deletions(-) delete mode 100644 respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryItem.kt create mode 100644 respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryViewModel.kt diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt index e19d9cfff..7c95e5ed1 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt @@ -1,13 +1,24 @@ package world.respect.app.view.apps.changehistory import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kotlinx.datetime.TimeZone +import org.jetbrains.compose.resources.stringResource +import world.respect.datalayer.db.school.ext.fullName +import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.change_format +import world.respect.shared.util.rememberFormattedDateTime import world.respect.shared.viewmodel.apps.changehistory.ChangeHistoryUiState import world.respect.shared.viewmodel.apps.changehistory.ChangeHistoryViewModel @@ -27,13 +38,42 @@ fun ChangeHistoryScreen( fun ChangeHistoryScreen( uiState: ChangeHistoryUiState, ) { + val changeHistoryEntryWithWhoDid = uiState.changeHistoryEntryWithWhoDid ?: return + Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) + .padding(16.dp) ) { + changeHistoryEntryWithWhoDid.forEach { group -> - } + group.changeHistoryEntry.forEach { entry -> + + entry.changes.forEach { change -> + Text( + text = group.person.fullName(), + ) + val createdAtStr = rememberFormattedDateTime( + timeInMillis = entry.timestamp, + timeZoneId = TimeZone.currentSystemDefault().id, + ) + + Text(text = createdAtStr) + Text( + text = stringResource( + Res.string.change_format, + change.field.displayName, + change.oldVal?:"", + change.newVal + ) + ) + Spacer(modifier = Modifier.height(16.dp)) + + } + } + } + } } \ No newline at end of file diff --git a/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/12.json b/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/12.json index 4ea7dfc1a..b2794f395 100644 --- a/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/12.json +++ b/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/12.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 12, - "identityHash": "a1d2a8d27768ea0e89527afe85c4f600", + "identityHash": "1999b1fad06789f3d63753bbad6a8283", "entities": [ { "tableName": "SchoolAppEntity", @@ -1215,7 +1215,7 @@ }, { "tableName": "ChangeHistoryEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hGuid` TEXT NOT NULL, `hGuidHash` INTEGER NOT NULL, `hTable` TEXT NOT NULL, `hTimestamp` INTEGER NOT NULL, `hWhoGuid` TEXT NOT NULL, `hWhoGuidHash` INTEGER NOT NULL, `hChanges` TEXT NOT NULL, PRIMARY KEY(`hGuidHash`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hGuid` TEXT NOT NULL, `hGuidHash` INTEGER NOT NULL, `hTable` TEXT NOT NULL, `hTableGuid` TEXT NOT NULL, `hTimestamp` INTEGER NOT NULL, `hWhoGuid` TEXT NOT NULL, `hWhoGuidHash` INTEGER NOT NULL, PRIMARY KEY(`hGuidHash`))", "fields": [ { "fieldPath": "hGuid", @@ -1235,6 +1235,12 @@ "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "hTableGuid", + "columnName": "hTableGuid", + "affinity": "TEXT", + "notNull": true + }, { "fieldPath": "hTimestamp", "columnName": "hTimestamp", @@ -1252,12 +1258,6 @@ "columnName": "hWhoGuidHash", "affinity": "INTEGER", "notNull": true - }, - { - "fieldPath": "hChanges", - "columnName": "hChanges", - "affinity": "TEXT", - "notNull": true } ], "primaryKey": { @@ -1311,7 +1311,7 @@ ], "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, 'a1d2a8d27768ea0e89527afe85c4f600')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1999b1fad06789f3d63753bbad6a8283')" ] } } \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabaseMigrations.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabaseMigrations.kt index 19123abe2..956ab995e 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabaseMigrations.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabaseMigrations.kt @@ -124,7 +124,7 @@ val MIGRATE_11_12 = object : Migration(11, 12) { `hTimestamp` INTEGER NOT NULL, `hWhoGuid` TEXT NOT NULL, `hWhoGuidHash` INTEGER NOT NULL, - `hChanges` TEXT NOT NULL, + `hTableGuid` TEXT NOT NULL, PRIMARY KEY(`hGuidHash`) ) """.trimIndent()) diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt index 8ea16e80c..71738e716 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt @@ -28,11 +28,13 @@ class ChangeHistoryDataSourceDb( override suspend fun findByGuid( loadParams: DataLoadParams, guid: String - ): DataLoadState { - return schoolDb.getChangeHistoryDao() - .findByGuid(guid)?.let { - DataReadyState(it.toModel()) - }?: NoDataLoadedState.notFound() + ): DataLoadState> { + val result = schoolDb.getChangeHistoryDao().findByGuid(guid) + return if (!result.isNullOrEmpty()) { + DataReadyState(result.map { it.toModel() }) + } else { + NoDataLoadedState.notFound() + } } override fun findByGuidAsFlow( diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/PersonDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/PersonDataSourceDb.kt index fbaf42121..85f6921f6 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/PersonDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/PersonDataSourceDb.kt @@ -93,23 +93,23 @@ class PersonDataSourceDb( } val timeNow = systemTimeInMillis() - val changeEntries = list.mapNotNull { person -> - - val oldPerson = schoolDb.getPersonEntityDao().findByGuidNum( - guidHash = uidNumberMapper(person.guid) + val oldPerson = schoolDb.getPersonEntityDao().findByGuidNum( + guidHash = uidNumberMapper(personToStore.guid) + ) + if (oldPerson != null) { + val changeEntries = generatePersonChanges( + hGuid = personToStore.guid, + old = oldPerson.toPersonEntities().toModel(), + new = personToStore, + whoGuid = authenticatedUser.guid, + timestamp = timeNow, + hTableGuid = personToStore.guid ) - generatePersonChanges( - hGuid = person.guid, - old = oldPerson?.toPersonEntities()?.toModel(), - new = person, - whoGuid = "", - timestamp = timeNow - ) - } - if (changeEntries.isNotEmpty()) { - changeHistoryDataSource.store(changeEntries) + if (changeEntries != null) { + changeHistoryDataSource.store(listOf(changeEntries)) + } } //Check that roles have not been change doUpsertPerson(personToStore) diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt index 79a131bd8..7375e7134 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt @@ -24,6 +24,7 @@ fun ChangeHistoryWithChanges.toModel(): ChangeHistoryEntry { table = history.hTable, timestamp = history.hTimestamp, whoGuid = history.hWhoGuid, + tableGuid = history.hTableGuid, changes = changes.map { it.toModel() } ) } @@ -50,7 +51,7 @@ fun ChangeHistoryEntry.toEntities( hTimestamp = timestamp, hWhoGuid = whoGuid, hWhoGuidHash = uidNumberMapper(whoGuid), - hChanges = "" + hTableGuid = tableGuid ) val changeEntities = changes.map { change -> @@ -72,7 +73,8 @@ fun generatePersonChanges( old: Person?, new: Person, whoGuid: String, - timestamp: Long + timestamp: Long, + hTableGuid: String ): ChangeHistoryEntry? { val changes = mutableListOf() @@ -93,6 +95,7 @@ fun generatePersonChanges( table = ChangeHistoryTableEnum.PERSON, timestamp = timestamp, whoGuid = whoGuid, - changes = changes + changes = changes, + tableGuid = hTableGuid ) } \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt index a964e76d8..ef4a2362e 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt @@ -47,12 +47,14 @@ interface ChangeHistoryDao { whoGuidHash: Long, ): PagingSource - @Query(""" + @Query( + """ SELECT * FROM ChangeHistoryEntity - WHERE hGuid = :guid - """) - suspend fun findByGuid(guid: String): ChangeHistoryWithChanges? + WHERE hTableGuid = :tableGuid + """ + ) + suspend fun findByGuid(tableGuid: String): List? @Query(""" diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryEntity.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryEntity.kt index 9f0c17369..0ee0b6e28 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryEntity.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryEntity.kt @@ -10,10 +10,10 @@ data class ChangeHistoryEntity( @PrimaryKey val hGuidHash: Long, val hTable: ChangeHistoryTableEnum, + val hTableGuid: String, val hTimestamp: Long, val hWhoGuid: String, val hWhoGuidHash: Long, - val hChanges: String ){ companion object{ const val PARENT_COLUMN = "hGuidHash" diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ChangeHistoryDataSourceHttp.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ChangeHistoryDataSourceHttp.kt index ea5e0b60c..35748fba6 100644 --- a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ChangeHistoryDataSourceHttp.kt +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ChangeHistoryDataSourceHttp.kt @@ -51,7 +51,7 @@ class ChangeHistoryDataSourceHttp( override suspend fun findByGuid( loadParams: DataLoadParams, guid: String - ): DataLoadState { + ): DataLoadState> { return httpClient.getAsDataLoadState>( ChangeHistoryDataSource.GetListParams( common = GetListCommonParams(guid = guid) @@ -59,7 +59,7 @@ class ChangeHistoryDataSourceHttp( ) { useTokenProvider(tokenProvider) useValidationCacheControl(validationHelper) - }.firstOrNotLoaded() + } } override fun findByGuidAsFlow( diff --git a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/ChangeHistoryDataSourceRepository.kt b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/ChangeHistoryDataSourceRepository.kt index 75c890b47..f58d5a9ae 100644 --- a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/ChangeHistoryDataSourceRepository.kt +++ b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/ChangeHistoryDataSourceRepository.kt @@ -7,6 +7,7 @@ import world.respect.datalayer.DataLoadParams import world.respect.datalayer.DataLoadState import world.respect.datalayer.ext.combineWithRemote import world.respect.datalayer.ext.updateFromRemoteIfNeeded +import world.respect.datalayer.ext.updateFromRemoteListIfNeeded import world.respect.datalayer.networkvalidation.ExtendedDataSourceValidationHelper import world.respect.datalayer.repository.shared.paging.RepositoryPagingSourceFactory import world.respect.datalayer.repository.shared.paging.loadAndUpdateLocal2 @@ -29,8 +30,8 @@ class ChangeHistoryDataSourceRepository( override suspend fun findByGuid( loadParams: DataLoadParams, guid: String - ): DataLoadState { - local.updateFromRemoteIfNeeded( + ): DataLoadState>{ + local.updateFromRemoteListIfNeeded( remoteLoad = remote.findByGuid(loadParams, guid), validationHelper = validationHelper, ) diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/ChangeHistoryDataSource.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/ChangeHistoryDataSource.kt index 142c5dfd9..372642b90 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/ChangeHistoryDataSource.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/ChangeHistoryDataSource.kt @@ -38,7 +38,7 @@ interface ChangeHistoryDataSource : WritableDataSource { suspend fun findByGuid( loadParams: DataLoadParams, guid: String - ): DataLoadState + ): DataLoadState> fun findByGuidAsFlow( loadParams: DataLoadParams, diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt index 1d2e7f091..97c3d020e 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt @@ -5,12 +5,19 @@ import kotlinx.serialization.Serializable @Serializable data class ChangeHistoryEntry( val guid: String, + val tableGuid: String, val table: ChangeHistoryTableEnum, val timestamp: Long, val whoGuid: String, val changes: List ) +@Serializable +data class ChangeHistoryEntryWithWhoDid( + val person: Person, + val changeHistoryEntry: List +) + @Serializable data class ChangeHistoryChange( val field: ChangeHistoryFieldEnum, diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryTableEnum.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryTableEnum.kt index 6d13fbe6b..1ae765e9b 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryTableEnum.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryTableEnum.kt @@ -12,29 +12,37 @@ enum class ChangeHistoryTableEnum(val value: String) { entries.first { it.value == value } } } -enum class ChangeHistoryFieldEnum(val value: String) { - - PERSON_GIVEN_NAME("pGivenName"), - PERSON_FAMILY_NAME("pFamilyName"), - PERSON_MIDDLE_NAME("pMiddleName"), - PERSON_USERNAME("pUsername"), - PERSON_GENDER("pGender"), - PERSON_EMAIL("pEmail"), - PERSON_PHONE_NUMBER("pPhoneNumber"), - PERSON_DATE_OF_BIRTH("pDateOfBirth"), - - CLASS_TITLE("cTitle"), - CLASS_DESCRIPTION("cDescription"), - CLASS_STATUS("cStatus"), +enum class ChangeHistoryFieldEnum( + val value: String, + val displayName: String +) { + + PERSON_GIVEN_NAME("pGivenName", "First Name"), + PERSON_FAMILY_NAME("pFamilyName", "Last Name"), + PERSON_MIDDLE_NAME("pMiddleName", "Middle Name"), + PERSON_USERNAME("pUsername", "Username"), + PERSON_GENDER("pGender", "Gender"), + PERSON_EMAIL("pEmail", "Email"), + PERSON_PHONE_NUMBER("pPhoneNumber", "Phone Number"), + PERSON_DATE_OF_BIRTH("pDateOfBirth", "Date of Birth"), + + CLASS_TITLE("cTitle", "Class Title"), + CLASS_DESCRIPTION("cDescription", "Class Description"), + CLASS_STATUS("cStatus", "Class Status"), + + ENROLLMENT_ROLE("eRole", "Role"), + ENROLLMENT_BEGIN_DATE("eBeginDate", "Start Date"), + ENROLLMENT_END_DATE("eEndDate", "End Date"), + ENROLLMENT_STATUS("eStatus", "Status"); + companion object { - ENROLLMENT_ROLE("eRole"), - ENROLLMENT_BEGIN_DATE("eBeginDate"), - ENROLLMENT_END_DATE("eEndDate"), - ENROLLMENT_STATUS("eStatus"); + fun fromValue(value: String): ChangeHistoryFieldEnum { + return entries.first { it.value == value } + } - companion object { - fun fromValue(value: String) = - entries.first { it.value == value } + fun fromValueOrNull(value: String): ChangeHistoryFieldEnum? { + return entries.find { it.value == value } + } } } \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index a219c3cb4..25255ec4b 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -570,5 +570,5 @@ Supported by Spix Foundation. Select Host - + %1$s changed from "%2$s" to "%3$s" diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index cc754ab53..806781f04 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -12,6 +12,7 @@ import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.Person import world.respect.shared.domain.account.invite.RespectRedeemInviteRequest import world.respect.datalayer.school.model.PersonRoleEnum +import world.respect.datalayer.school.model.ChangeHistoryTableEnum import world.respect.datalayer.school.model.report.ReportFilter import world.respect.shared.ext.NextAfterScan import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping @@ -867,4 +868,23 @@ data class CopyCode( @Serializable data class ChangeHistory( val guid: String, -): RespectAppRoute \ No newline at end of file + val table : String, +) : RespectAppRoute { + + @Transient + val tableValue = ChangeHistoryTableEnum.fromValue(table) + + + companion object { + fun create( + guidStr: String, + tableEnum: ChangeHistoryTableEnum, + ) : ChangeHistory { + return ChangeHistory( + guid = guidStr, + table = tableEnum.value, + ) + } + } + +} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryItem.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryItem.kt deleted file mode 100644 index 647b9e1d1..000000000 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryItem.kt +++ /dev/null @@ -1,49 +0,0 @@ -package world.respect.shared.viewmodel.apps.changehistory - -import androidx.lifecycle.SavedStateHandle -import androidx.navigation.toRoute -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import org.koin.core.component.KoinScopeComponent -import org.koin.core.scope.Scope -import world.respect.shared.domain.account.RespectAccountManager -import world.respect.shared.generated.resources.Res -import world.respect.shared.generated.resources.change_history -import world.respect.shared.navigation.ChangeHistory -import world.respect.shared.util.ext.asUiText -import world.respect.shared.viewmodel.RespectViewModel -import world.respect.shared.viewmodel.app.appstate.FabUiState - - -data class ChangeHistoryUiState( - val guid: String = "", -) - -class ChangeHistoryViewModel( - savedStateHandle: SavedStateHandle, - accountManager: RespectAccountManager -) : RespectViewModel(savedStateHandle), KoinScopeComponent { - - override val scope: Scope = accountManager.requireActiveAccountScope() - - private val route: ChangeHistory = savedStateHandle.toRoute() - - private val _uiState = MutableStateFlow( - ChangeHistoryUiState( - guid = route.guid - ) - ) - - val uiState = _uiState.asStateFlow() - - init { - _appUiState.update { prev -> - prev.copy( - title = Res.string.change_history.asUiText(), - fabState = FabUiState() - ) - } - } - -} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryViewModel.kt new file mode 100644 index 000000000..a745b2be0 --- /dev/null +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryViewModel.kt @@ -0,0 +1,104 @@ +package world.respect.shared.viewmodel.apps.changehistory + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.koin.core.component.KoinScopeComponent +import org.koin.core.component.inject +import org.koin.core.scope.Scope +import world.respect.datalayer.DataLoadParams +import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.ext.dataOrNull +import world.respect.datalayer.school.model.ChangeHistoryEntryWithWhoDid +import world.respect.datalayer.school.model.ChangeHistoryTableEnum +import world.respect.shared.domain.account.RespectAccountManager +import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.change_history +import world.respect.shared.navigation.ChangeHistory +import world.respect.shared.util.ext.asUiText +import world.respect.shared.viewmodel.RespectViewModel +import world.respect.shared.viewmodel.app.appstate.FabUiState +import kotlin.getValue + + +data class ChangeHistoryUiState( + val guid: String = "", + val changeHistoryEntryWithWhoDid: List? = null, +) + +class ChangeHistoryViewModel( + savedStateHandle: SavedStateHandle, + accountManager: RespectAccountManager +) : RespectViewModel(savedStateHandle), KoinScopeComponent { + private val schoolDataSource: SchoolDataSource by inject() + + override val scope: Scope = accountManager.requireActiveAccountScope() + + private val route: ChangeHistory = savedStateHandle.toRoute() + + private val _uiState = MutableStateFlow( + ChangeHistoryUiState( + guid = route.guid + ) + ) + + val uiState = _uiState.asStateFlow() + + init { + viewModelScope.launch { + when (route.tableValue) { + ChangeHistoryTableEnum.PERSON -> { + + val changeHistoryList = schoolDataSource.changeHistoryDataSource + .findByGuid( + loadParams = DataLoadParams(), + guid = route.guid, + ).dataOrNull() ?: return@launch + + val whoGuids = changeHistoryList.groupBy { it.whoGuid } + + val resultList = whoGuids.mapNotNull { (whoGuid, entries) -> + + val person = schoolDataSource.personDataSource.findByGuid( + loadParams = DataLoadParams(), + guid = whoGuid, + ).dataOrNull() + + person?.let { + ChangeHistoryEntryWithWhoDid( + person = it, + changeHistoryEntry = entries + ) + } + } + + _uiState.update { prev -> + prev.copy( + changeHistoryEntryWithWhoDid = resultList + ) + } + } + + ChangeHistoryTableEnum.CLASS -> { + + } + + ChangeHistoryTableEnum.ENROLLMENT -> { + + } + } + + } + _appUiState.update { prev -> + prev.copy( + title = Res.string.change_history.asUiText(), + fabState = FabUiState() + ) + } + } + +} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/detail/PersonDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/detail/PersonDetailViewModel.kt index e73fc081e..708129493 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/detail/PersonDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/detail/PersonDetailViewModel.kt @@ -21,6 +21,7 @@ import world.respect.datalayer.db.school.ext.isAdminOrTeacher import world.respect.datalayer.ext.dataOrNull import world.respect.datalayer.school.PersonDataSource import world.respect.datalayer.school.domain.CheckPersonPermissionUseCase +import world.respect.datalayer.school.model.ChangeHistoryTableEnum import world.respect.datalayer.school.model.Person import world.respect.datalayer.shared.params.GetListCommonParams import world.respect.shared.domain.account.RespectAccountManager @@ -171,7 +172,8 @@ class PersonDetailViewModel( _navCommandFlow.tryEmit( NavCommand.Navigate( ChangeHistory( - guid = "" + guid = route.guid, + table = ChangeHistoryTableEnum.PERSON.value ) ) ) From 34267a33ea96145063f482906814dfefc65bc1cb Mon Sep 17 00:00:00 2001 From: pooja Date: Wed, 18 Mar 2026 10:26:55 +0400 Subject: [PATCH 09/21] Updated first name placeholder text in Maestro flow --- .../flows/001_001_invite_users_using_qr_code_or_link_test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml index a603833fd..3ec8e3605 100644 --- a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml +++ b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml @@ -109,7 +109,7 @@ onFlowComplete: - runFlow: file: "subflows/erase_text.yaml" env: - TEXT: "First name*" + TEXT: "First names*" - inputText: "TeacherB" - runScript: file: "scripts/setDate.js" From 52c1d6f88c611d57df81bb0ab1a4ef6726a88cf6 Mon Sep 17 00:00:00 2001 From: pooja Date: Mon, 23 Mar 2026 08:56:30 +0400 Subject: [PATCH 10/21] Update label to "First names*" in invite users Maestro test flow. --- .../flows/001_001_invite_users_using_qr_code_or_link_test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml index 3ec8e3605..5bf4fab3b 100644 --- a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml +++ b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml @@ -105,7 +105,7 @@ onFlowComplete: - tapOn: "TeacherA User" - tapOn: id: "floating_action_button" # Edit button -- tapOn: "First name*" +- tapOn: "First names*" - runFlow: file: "subflows/erase_text.yaml" env: From c7adda4d233b47172b526e3e31c9e9297c9199dd Mon Sep 17 00:00:00 2001 From: pooja Date: Mon, 23 Mar 2026 11:45:14 +0400 Subject: [PATCH 11/21] Update Maestro test flow for inviting users to include verification of name change history. --- ..._001_invite_users_using_qr_code_or_link_test.yaml | 12 +++++++++--- .../subflows/{admin_add_app.yaml => add_app.yaml} | 0 .../{admin_add_class.yaml => add_class.yaml} | 0 .../{admin_add_student.yaml => add_student.yaml} | 0 4 files changed, 9 insertions(+), 3 deletions(-) rename .maestro/flows/subflows/{admin_add_app.yaml => add_app.yaml} (100%) rename .maestro/flows/subflows/{admin_add_class.yaml => add_class.yaml} (100%) rename .maestro/flows/subflows/{admin_add_student.yaml => add_student.yaml} (100%) diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml index 5bf4fab3b..ea5750c67 100644 --- a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml +++ b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml @@ -102,9 +102,9 @@ onFlowComplete: - assertVisible: "Apps" - assertVisible: "Assignments" - tapOn: "People" -- tapOn: "TeacherA User" + - tapOn: - id: "floating_action_button" # Edit button + id: "floating_action_bu- tapOn: "TeacherA User"tton" # Edit button - tapOn: "First names*" - runFlow: file: "subflows/erase_text.yaml" @@ -122,7 +122,7 @@ onFlowComplete: - assertVisible: "Person name changed from “TeacherA User” to “TeacherB User”" - runFlow: - file: "subflows/admin_add_class.yaml" + file: "subflows/add_class.yaml" env: CLASSNAME: "New Class" - assertVisible: @@ -225,6 +225,12 @@ onFlowComplete: text: "Change History" - assertVisible: "Join request approved for “Student User”" - assertVisible: "Teacher added: “TeacherB User”" +- tapOn: "TeacherB User" +- tapOn: "Change History" +- assertVisible: + id: "app_title" + text: "Change History" +- assertVisible: "Person name changed from “TeacherA User” to “TeacherB User”" # E) Teacher share invite link for parent device based signup - tapOn: "Add student" diff --git a/.maestro/flows/subflows/admin_add_app.yaml b/.maestro/flows/subflows/add_app.yaml similarity index 100% rename from .maestro/flows/subflows/admin_add_app.yaml rename to .maestro/flows/subflows/add_app.yaml diff --git a/.maestro/flows/subflows/admin_add_class.yaml b/.maestro/flows/subflows/add_class.yaml similarity index 100% rename from .maestro/flows/subflows/admin_add_class.yaml rename to .maestro/flows/subflows/add_class.yaml diff --git a/.maestro/flows/subflows/admin_add_student.yaml b/.maestro/flows/subflows/add_student.yaml similarity index 100% rename from .maestro/flows/subflows/admin_add_student.yaml rename to .maestro/flows/subflows/add_student.yaml From 836f221292a0a8f6cbb3b6c289520d63c1ac4478 Mon Sep 17 00:00:00 2001 From: lenovo Date: Tue, 24 Mar 2026 23:51:08 +0530 Subject: [PATCH 12/21] commit --- .../kotlin/world/respect/AppKoinModule.kt | 20 +- .../view/clazz/detail/ClazzDetailScreen.kt | 18 + .../13.json | 1838 ++++++++++++++++ .../14.json | 1868 +++++++++++++++++ .../datalayer/db/RespectSchoolDatabase.kt | 4 +- .../db/RespectSchoolDatabaseMigrations.kt | 14 +- .../datalayer/db/SchoolDataSourceDb.kt | 11 +- .../db/school/ChangeHistoryDataSourceDb.kt | 44 +- .../school/ChangeHistoryMarkSentToServerDb.kt | 24 + .../db/school/ChangeHistoryProviderDb.kt | 24 + .../datalayer/db/school/ClassDatasourceDb.kt | 26 +- .../datalayer/db/school/PersonDataSourceDb.kt | 12 +- .../school/adapters/ChangeHistoryAdapter.kt | 74 +- .../db/school/daos/ChangeHistoryDao.kt | 47 +- .../entities/ChangeHistoryChangeEntity.kt | 8 +- .../db/school/entities/ChangeHistoryEntity.kt | 4 + .../datalayer/http/SchoolDataSourceHttp.kt | 6 + .../school/ChangeHistoryDataSourceHttp.kt | 39 +- .../http/school/DataAndChangeHistory.kt | 9 + .../http/school/PersonDataSourceHttp.kt | 44 +- .../ChangeHistoryDataSourceRepository.kt | 25 +- .../school/PersonDataSourceRepository.kt | 8 + .../DrainRemoteWriteQueueUseCase.kt | 2 +- .../ChangeHistoryMarkSentToServer.kt | 9 + .../datalayer/ChangeHistoryProvider.kt | 12 + .../school/ChangeHistoryDataSource.kt | 12 +- .../datalayer/school/PersonDataSource.kt | 6 +- .../domain/CheckPersonPermissionUseCase.kt | 7 +- .../school/model/ChangeHistoryEntry.kt | 26 +- .../school/model/PersonWithHistoryRequest.kt | 9 + .../datalayer/school/model/findDifference.kt | 2 +- .../changehistory/ChangeHistoryViewModel.kt | 45 +- .../clazz/detail/ClazzDetailViewModel.kt | 18 +- .../person/detail/PersonDetailViewModel.kt | 8 +- .../routes/school/respect/PersonRoute.kt | 33 +- 35 files changed, 4284 insertions(+), 72 deletions(-) create mode 100644 respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/13.json create mode 100644 respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/14.json create mode 100644 respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryMarkSentToServerDb.kt create mode 100644 respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryProviderDb.kt create mode 100644 respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/DataAndChangeHistory.kt create mode 100644 respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/ChangeHistoryMarkSentToServer.kt create mode 100644 respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/ChangeHistoryProvider.kt create mode 100644 respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/PersonWithHistoryRequest.kt diff --git a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt index ad08a25c4..9f0bc23b7 100644 --- a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt +++ b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt @@ -56,6 +56,8 @@ import world.respect.credentials.passkey.request.GetPasskeyProviderInfoUseCase import world.respect.credentials.password.SavePasswordUseCaseAndroidImpl import world.respect.datalayer.AuthTokenProvider import world.respect.datalayer.AuthenticatedUserPrincipalId +import world.respect.datalayer.ChangeHistoryProvider +import world.respect.datalayer.ChangeHistoryMarkSentToServer import world.respect.datalayer.RespectAppDataSource import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.SchoolDataSourceLocal @@ -66,6 +68,8 @@ import world.respect.datalayer.db.RespectSchoolDatabase import world.respect.datalayer.db.SchoolDataSourceDb import world.respect.datalayer.db.addCommonMigrations import world.respect.datalayer.db.networkvalidation.ExtendedDataSourceValidationHelperImpl +import world.respect.datalayer.db.school.ChangeHistoryProviderDb +import world.respect.datalayer.db.school.ChangeHistoryMarkSentToServerDb import world.respect.datalayer.db.school.GetAuthenticatedPersonUseCase import world.respect.datalayer.db.school.domain.CheckPersonPermissionUseCaseDbImpl import world.respect.datalayer.db.school.writequeue.RemoteWriteQueueDbImpl @@ -901,7 +905,17 @@ val appKoinModule = module { defaultAppCatalogUrl = RespectBuildConfig.RESPECT_DEFAULT_APPLIST, ) } - + scoped { + ChangeHistoryProviderDb( + schoolDb = get(), + ) + } + scoped { + ChangeHistoryMarkSentToServerDb( + schoolDb = get(), + uidNumberMapper = get(), + ) + } scoped { val schoolUrl = get() val localDs = get() @@ -918,7 +932,9 @@ val appKoinModule = module { defaultAppCatalogUrl = RespectBuildConfig.RESPECT_DEFAULT_APPLIST, opdsFeedValidationHelper = localDs.opdsFeedDataSource, opdsPublicationValidationHelper = localDs.opdsPublicationDataSource - .publicationNetworkValidationHelper + .publicationNetworkValidationHelper, + changeHistoryProvider = get(), + markSentToServer = get(), ), validationHelper = get(), remoteWriteQueue = get(), diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 09f903975..4e9f7010b 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -59,9 +59,13 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.IconButton import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Replay +import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import world.respect.app.components.RespectQuickActionButton +import world.respect.shared.generated.resources.change_history import world.respect.shared.generated.resources.manage_enrollments import world.respect.shared.generated.resources.more_options import world.respect.shared.generated.resources.remove_from_class @@ -83,6 +87,7 @@ fun ClazzDetailScreen( onTogglePendingSection = viewModel::onTogglePendingSection, onToggleTeachersSection = viewModel::onToggleTeachersSection, onToggleStudentsSection = viewModel::onToggleStudentsSection, + onClickChangeHistoryButton = viewModel::onClickChangeHistoryButton, onClickRemovePersonFromClass = viewModel::onClickRemovePersonFromClass, onClickManageEnrollments = viewModel::onClickManageEnrollments, onClickPerson = viewModel::onClickPerson, @@ -97,6 +102,7 @@ fun ClazzDetailScreen( onSelectChip: (String) -> Unit, onClickAcceptInvite: (Person) -> Unit, onClickDismissInvite: (Person) -> Unit, + onClickChangeHistoryButton: () -> Unit, onTogglePendingSection: () -> Unit, onToggleTeachersSection: () -> Unit, onToggleStudentsSection: () -> Unit, @@ -141,6 +147,18 @@ fun ClazzDetailScreen( } ) } + item { + if(uiState.changeHistoryButtonVisible){ + HorizontalDivider() + RespectQuickActionButton( + labelText = stringResource(Res.string.change_history), + imageVector = Icons.Default.Replay, + onClick = onClickChangeHistoryButton, + ) + HorizontalDivider() + } + } + item { RespectListSortHeader( diff --git a/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/13.json b/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/13.json new file mode 100644 index 000000000..7521c2a09 --- /dev/null +++ b/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/13.json @@ -0,0 +1,1838 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "a92de10409147409c00afbdef34ced32", + "entities": [ + { + "tableName": "SchoolAppEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`saUid` TEXT NOT NULL, `saUidNum` INTEGER NOT NULL, `saManifestUrl` TEXT NOT NULL, `saStatus` INTEGER NOT NULL, `saLastModified` INTEGER NOT NULL, `saStored` INTEGER NOT NULL, PRIMARY KEY(`saUidNum`))", + "fields": [ + { + "fieldPath": "saUid", + "columnName": "saUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "saUidNum", + "columnName": "saUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "saManifestUrl", + "columnName": "saManifestUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "saStatus", + "columnName": "saStatus", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "saLastModified", + "columnName": "saLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "saStored", + "columnName": "saStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "saUidNum" + ] + } + }, + { + "tableName": "PersonEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pGuid` TEXT NOT NULL, `pGuidHash` INTEGER NOT NULL, `pActive` INTEGER NOT NULL, `pStatus` INTEGER NOT NULL, `pLastModified` INTEGER NOT NULL, `pStored` INTEGER NOT NULL, `pMetadata` TEXT, `pUsername` TEXT, `pGivenName` TEXT NOT NULL, `pFamilyName` TEXT NOT NULL, `pMiddleName` TEXT, `pGender` INTEGER NOT NULL, `pDateOfBirth` INTEGER, `pEmail` TEXT, `pPhoneNumber` TEXT, PRIMARY KEY(`pGuidHash`))", + "fields": [ + { + "fieldPath": "pGuid", + "columnName": "pGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pGuidHash", + "columnName": "pGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pActive", + "columnName": "pActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pStatus", + "columnName": "pStatus", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pLastModified", + "columnName": "pLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pStored", + "columnName": "pStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pMetadata", + "columnName": "pMetadata", + "affinity": "TEXT" + }, + { + "fieldPath": "pUsername", + "columnName": "pUsername", + "affinity": "TEXT" + }, + { + "fieldPath": "pGivenName", + "columnName": "pGivenName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pFamilyName", + "columnName": "pFamilyName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pMiddleName", + "columnName": "pMiddleName", + "affinity": "TEXT" + }, + { + "fieldPath": "pGender", + "columnName": "pGender", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pDateOfBirth", + "columnName": "pDateOfBirth", + "affinity": "INTEGER" + }, + { + "fieldPath": "pEmail", + "columnName": "pEmail", + "affinity": "TEXT" + }, + { + "fieldPath": "pPhoneNumber", + "columnName": "pPhoneNumber", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "pGuidHash" + ] + } + }, + { + "tableName": "PersonRoleEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`prUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `prPersonGuidHash` INTEGER NOT NULL, `prIsPrimaryRole` INTEGER NOT NULL, `prRoleEnum` INTEGER NOT NULL, `prBeginDate` INTEGER, `prEndDate` INTEGER)", + "fields": [ + { + "fieldPath": "prUid", + "columnName": "prUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prPersonGuidHash", + "columnName": "prPersonGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prIsPrimaryRole", + "columnName": "prIsPrimaryRole", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prRoleEnum", + "columnName": "prRoleEnum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prBeginDate", + "columnName": "prBeginDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "prEndDate", + "columnName": "prEndDate", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "prUid" + ] + } + }, + { + "tableName": "PersonRelatedPersonEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`prpUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `prpPersonUidNum` INTEGER NOT NULL, `prpOtherPersonUid` TEXT NOT NULL, `prpOtherPersonUidNum` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "prpUid", + "columnName": "prpUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prpPersonUidNum", + "columnName": "prpPersonUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prpOtherPersonUid", + "columnName": "prpOtherPersonUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "prpOtherPersonUidNum", + "columnName": "prpOtherPersonUidNum", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "prpUid" + ] + } + }, + { + "tableName": "PersonPasswordEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ppwGuidNum` INTEGER NOT NULL, `ppwGuid` TEXT NOT NULL, `authAlgorithm` TEXT NOT NULL, `authEncoded` TEXT NOT NULL, `authSalt` TEXT NOT NULL, `authIterations` INTEGER NOT NULL, `authKeyLen` INTEGER NOT NULL, `ppwLastModified` INTEGER NOT NULL, `ppwStored` INTEGER NOT NULL, PRIMARY KEY(`ppwGuidNum`))", + "fields": [ + { + "fieldPath": "ppwGuidNum", + "columnName": "ppwGuidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppwGuid", + "columnName": "ppwGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authAlgorithm", + "columnName": "authAlgorithm", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authEncoded", + "columnName": "authEncoded", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authSalt", + "columnName": "authSalt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authIterations", + "columnName": "authIterations", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authKeyLen", + "columnName": "authKeyLen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppwLastModified", + "columnName": "ppwLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppwStored", + "columnName": "ppwStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "ppwGuidNum" + ] + } + }, + { + "tableName": "PersonPasskeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ppPersonUidNum` INTEGER NOT NULL, `ppCredentialId` TEXT NOT NULL, `ppLastModified` INTEGER NOT NULL, `ppStored` INTEGER NOT NULL, `ppAttestationObj` TEXT, `ppClientDataJson` TEXT, `ppOriginString` TEXT, `ppChallengeString` TEXT, `ppPublicKey` TEXT, `isRevoked` INTEGER NOT NULL, `ppDeviceName` TEXT NOT NULL DEFAULT '', `ppTimeCreated` INTEGER NOT NULL DEFAULT 0, `ppAaguid` TEXT NOT NULL DEFAULT '', `ppProviderName` TEXT NOT NULL DEFAULT '', `ppIconLight` TEXT NOT NULL DEFAULT '', `ppIconDark` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`ppPersonUidNum`, `ppCredentialId`))", + "fields": [ + { + "fieldPath": "ppPersonUidNum", + "columnName": "ppPersonUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppCredentialId", + "columnName": "ppCredentialId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ppLastModified", + "columnName": "ppLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppStored", + "columnName": "ppStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppAttestationObj", + "columnName": "ppAttestationObj", + "affinity": "TEXT" + }, + { + "fieldPath": "ppClientDataJson", + "columnName": "ppClientDataJson", + "affinity": "TEXT" + }, + { + "fieldPath": "ppOriginString", + "columnName": "ppOriginString", + "affinity": "TEXT" + }, + { + "fieldPath": "ppChallengeString", + "columnName": "ppChallengeString", + "affinity": "TEXT" + }, + { + "fieldPath": "ppPublicKey", + "columnName": "ppPublicKey", + "affinity": "TEXT" + }, + { + "fieldPath": "isRevoked", + "columnName": "isRevoked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppDeviceName", + "columnName": "ppDeviceName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "ppTimeCreated", + "columnName": "ppTimeCreated", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ppAaguid", + "columnName": "ppAaguid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "ppProviderName", + "columnName": "ppProviderName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "ppIconLight", + "columnName": "ppIconLight", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "ppIconDark", + "columnName": "ppIconDark", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "ppPersonUidNum", + "ppCredentialId" + ] + } + }, + { + "tableName": "AuthTokenEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`atUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `atPGuidHash` INTEGER NOT NULL, `atPGuid` TEXT NOT NULL, `atCode` TEXT, `atToken` TEXT NOT NULL, `atTimeCreated` INTEGER NOT NULL, `atTtl` INTEGER NOT NULL, `atPlatform` TEXT, `atAndroidSdkInt` INTEGER, `atVersion` TEXT, `atManufacturer` TEXT, `atModel` TEXT, `atRam` INTEGER)", + "fields": [ + { + "fieldPath": "atUid", + "columnName": "atUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "atPGuidHash", + "columnName": "atPGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "atPGuid", + "columnName": "atPGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "atCode", + "columnName": "atCode", + "affinity": "TEXT" + }, + { + "fieldPath": "atToken", + "columnName": "atToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "atTimeCreated", + "columnName": "atTimeCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "atTtl", + "columnName": "atTtl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "atPlatform", + "columnName": "atPlatform", + "affinity": "TEXT" + }, + { + "fieldPath": "atAndroidSdkInt", + "columnName": "atAndroidSdkInt", + "affinity": "INTEGER" + }, + { + "fieldPath": "atVersion", + "columnName": "atVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "atManufacturer", + "columnName": "atManufacturer", + "affinity": "TEXT" + }, + { + "fieldPath": "atModel", + "columnName": "atModel", + "affinity": "TEXT" + }, + { + "fieldPath": "atRam", + "columnName": "atRam", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "atUid" + ] + } + }, + { + "tableName": "ReportEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rGuid` TEXT NOT NULL, `rOwnerGuid` TEXT NOT NULL, `rTitle` TEXT NOT NULL, `rOptions` TEXT NOT NULL, `rIsTemplate` INTEGER NOT NULL, `rActive` INTEGER NOT NULL, `rLastModified` INTEGER NOT NULL, `rStored` INTEGER NOT NULL, PRIMARY KEY(`rGuid`))", + "fields": [ + { + "fieldPath": "rGuid", + "columnName": "rGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rOwnerGuid", + "columnName": "rOwnerGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rTitle", + "columnName": "rTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rOptions", + "columnName": "rOptions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rIsTemplate", + "columnName": "rIsTemplate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rActive", + "columnName": "rActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rLastModified", + "columnName": "rLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rStored", + "columnName": "rStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "rGuid" + ] + } + }, + { + "tableName": "IndicatorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`indicatorId` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, `type` TEXT NOT NULL, `sql` TEXT NOT NULL, PRIMARY KEY(`indicatorId`))", + "fields": [ + { + "fieldPath": "indicatorId", + "columnName": "indicatorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sql", + "columnName": "sql", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "indicatorId" + ] + } + }, + { + "tableName": "ClassEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cGuid` TEXT NOT NULL, `cGuidHash` INTEGER NOT NULL, `cTitle` TEXT NOT NULL, `cStatus` INTEGER NOT NULL, `cDescription` TEXT, `cLastModified` INTEGER NOT NULL, `cStored` INTEGER NOT NULL, `cTeacherInviteGuid` TEXT, `cStudentInviteGuid` TEXT, PRIMARY KEY(`cGuidHash`))", + "fields": [ + { + "fieldPath": "cGuid", + "columnName": "cGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cGuidHash", + "columnName": "cGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cTitle", + "columnName": "cTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cStatus", + "columnName": "cStatus", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cDescription", + "columnName": "cDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "cLastModified", + "columnName": "cLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cStored", + "columnName": "cStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cTeacherInviteGuid", + "columnName": "cTeacherInviteGuid", + "affinity": "TEXT" + }, + { + "fieldPath": "cStudentInviteGuid", + "columnName": "cStudentInviteGuid", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "cGuidHash" + ] + } + }, + { + "tableName": "ClassPermissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cpeId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `cpeClassUidNum` INTEGER NOT NULL, `cpeToEnrollmentRole` INTEGER, `cpePermissions` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "cpeId", + "columnName": "cpeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cpeClassUidNum", + "columnName": "cpeClassUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cpeToEnrollmentRole", + "columnName": "cpeToEnrollmentRole", + "affinity": "INTEGER" + }, + { + "fieldPath": "cpePermissions", + "columnName": "cpePermissions", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "cpeId" + ] + } + }, + { + "tableName": "EnrollmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eUid` TEXT NOT NULL, `eUidNum` INTEGER NOT NULL, `eStatus` INTEGER NOT NULL, `eLastModified` INTEGER NOT NULL, `eStored` INTEGER NOT NULL, `eMetadata` TEXT, `eClassUid` TEXT NOT NULL, `eClassUidNum` INTEGER NOT NULL, `ePersonUid` TEXT NOT NULL, `ePersonUidNum` INTEGER NOT NULL, `eRole` INTEGER NOT NULL, `eBeginDate` INTEGER, `eEndDate` INTEGER, `eRemovedAt` INTEGER, `eInviteCode` TEXT, `eApprovedByPersonUidNum` INTEGER NOT NULL, `eApprovedByPersonUid` TEXT, PRIMARY KEY(`eUidNum`))", + "fields": [ + { + "fieldPath": "eUid", + "columnName": "eUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eUidNum", + "columnName": "eUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eStatus", + "columnName": "eStatus", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eLastModified", + "columnName": "eLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eStored", + "columnName": "eStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eMetadata", + "columnName": "eMetadata", + "affinity": "TEXT" + }, + { + "fieldPath": "eClassUid", + "columnName": "eClassUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eClassUidNum", + "columnName": "eClassUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ePersonUid", + "columnName": "ePersonUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ePersonUidNum", + "columnName": "ePersonUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eRole", + "columnName": "eRole", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eBeginDate", + "columnName": "eBeginDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "eEndDate", + "columnName": "eEndDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "eRemovedAt", + "columnName": "eRemovedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "eInviteCode", + "columnName": "eInviteCode", + "affinity": "TEXT" + }, + { + "fieldPath": "eApprovedByPersonUidNum", + "columnName": "eApprovedByPersonUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eApprovedByPersonUid", + "columnName": "eApprovedByPersonUid", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "eUidNum" + ] + } + }, + { + "tableName": "AssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`aeUid` TEXT NOT NULL, `aeUidNum` INTEGER NOT NULL, `aeTitle` TEXT NOT NULL, `aeDescription` TEXT NOT NULL, `aeClassUid` TEXT NOT NULL, `aeClassUidNum` INTEGER NOT NULL, `aeDeadline` INTEGER, `aeLastModified` INTEGER NOT NULL, `aeStored` INTEGER NOT NULL, PRIMARY KEY(`aeUidNum`))", + "fields": [ + { + "fieldPath": "aeUid", + "columnName": "aeUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "aeUidNum", + "columnName": "aeUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "aeTitle", + "columnName": "aeTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "aeDescription", + "columnName": "aeDescription", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "aeClassUid", + "columnName": "aeClassUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "aeClassUidNum", + "columnName": "aeClassUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "aeDeadline", + "columnName": "aeDeadline", + "affinity": "INTEGER" + }, + { + "fieldPath": "aeLastModified", + "columnName": "aeLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "aeStored", + "columnName": "aeStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "aeUidNum" + ] + } + }, + { + "tableName": "AssignmentLearningResourceRefEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`alrrAeUidNum` INTEGER NOT NULL, `alrrLearningUnitManifestUrlHash` INTEGER NOT NULL, `alrrLearningUnitManifestUrl` TEXT NOT NULL, `alrrAppManifestUrl` TEXT NOT NULL, PRIMARY KEY(`alrrAeUidNum`, `alrrLearningUnitManifestUrlHash`))", + "fields": [ + { + "fieldPath": "alrrAeUidNum", + "columnName": "alrrAeUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alrrLearningUnitManifestUrlHash", + "columnName": "alrrLearningUnitManifestUrlHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alrrLearningUnitManifestUrl", + "columnName": "alrrLearningUnitManifestUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alrrAppManifestUrl", + "columnName": "alrrAppManifestUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "alrrAeUidNum", + "alrrLearningUnitManifestUrlHash" + ] + } + }, + { + "tableName": "WriteQueueItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`wqiQueueItemId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `wqiModel` INTEGER NOT NULL, `wqiUid` TEXT NOT NULL, `wqiTimeQueued` INTEGER NOT NULL, `wqiAttemptCount` INTEGER NOT NULL, `wqiTimeWritten` INTEGER NOT NULL, `wqiAccountGuid` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "wqiQueueItemId", + "columnName": "wqiQueueItemId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wqiModel", + "columnName": "wqiModel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wqiUid", + "columnName": "wqiUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wqiTimeQueued", + "columnName": "wqiTimeQueued", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wqiAttemptCount", + "columnName": "wqiAttemptCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wqiTimeWritten", + "columnName": "wqiTimeWritten", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wqiAccountGuid", + "columnName": "wqiAccountGuid", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "wqiQueueItemId" + ] + } + }, + { + "tableName": "SchoolPermissionGrantEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`spgUid` TEXT NOT NULL, `spgUidNum` INTEGER NOT NULL, `spgStatusEnum` INTEGER NOT NULL, `spgToRole` INTEGER NOT NULL, `spgPermissions` INTEGER NOT NULL, `spgLastModified` INTEGER NOT NULL, `spgStored` INTEGER NOT NULL, PRIMARY KEY(`spgUidNum`))", + "fields": [ + { + "fieldPath": "spgUid", + "columnName": "spgUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "spgUidNum", + "columnName": "spgUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spgStatusEnum", + "columnName": "spgStatusEnum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spgToRole", + "columnName": "spgToRole", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spgPermissions", + "columnName": "spgPermissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spgLastModified", + "columnName": "spgLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spgStored", + "columnName": "spgStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "spgUidNum" + ] + } + }, + { + "tableName": "PullSyncStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pssAccountPersonUid` TEXT NOT NULL, `pssAccountPersonUidNum` INTEGER NOT NULL, `pssTableId` INTEGER NOT NULL, `pssLastConsistentThrough` INTEGER NOT NULL, `pssPermissionsLastModified` INTEGER NOT NULL, PRIMARY KEY(`pssAccountPersonUid`, `pssTableId`))", + "fields": [ + { + "fieldPath": "pssAccountPersonUid", + "columnName": "pssAccountPersonUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pssAccountPersonUidNum", + "columnName": "pssAccountPersonUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pssTableId", + "columnName": "pssTableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pssLastConsistentThrough", + "columnName": "pssLastConsistentThrough", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pssPermissionsLastModified", + "columnName": "pssPermissionsLastModified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "pssAccountPersonUid", + "pssTableId" + ] + } + }, + { + "tableName": "PersonQrBadgeEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pqrGuidNum` INTEGER NOT NULL, `pqrGuid` TEXT NOT NULL, `pqrLastModified` INTEGER NOT NULL, `pqrStored` INTEGER NOT NULL, `pqrQrCodeUrl` TEXT, `pqrStatus` INTEGER NOT NULL, PRIMARY KEY(`pqrGuidNum`))", + "fields": [ + { + "fieldPath": "pqrGuidNum", + "columnName": "pqrGuidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pqrGuid", + "columnName": "pqrGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pqrLastModified", + "columnName": "pqrLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pqrStored", + "columnName": "pqrStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pqrQrCodeUrl", + "columnName": "pqrQrCodeUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "pqrStatus", + "columnName": "pqrStatus", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "pqrGuidNum" + ] + }, + "indices": [ + { + "name": "index_PersonQrBadgeEntity_pqrQrCodeUrl", + "unique": false, + "columnNames": [ + "pqrQrCodeUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PersonQrBadgeEntity_pqrQrCodeUrl` ON `${TABLE_NAME}` (`pqrQrCodeUrl`)" + } + ] + }, + { + "tableName": "InviteEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`iGuid` TEXT NOT NULL, `iGuidHash` INTEGER NOT NULL, `iCode` TEXT NOT NULL, `iApprovalRequiredAfter` INTEGER NOT NULL, `iLastModified` INTEGER NOT NULL, `iStored` INTEGER NOT NULL, `iStatus` INTEGER NOT NULL, `iNewUserRole` INTEGER, `iNewUserFirstInvite` INTEGER NOT NULL, `iForFamilyOfGuid` TEXT, `iForFamilyOfGuidHash` INTEGER, `iForClassGuid` TEXT, `iForClassName` TEXT, `iInviteMode` INTEGER, `iSchoolName` TEXT, `iForClassGuidHash` INTEGER, `iForClassRole` INTEGER, PRIMARY KEY(`iGuidHash`))", + "fields": [ + { + "fieldPath": "iGuid", + "columnName": "iGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iGuidHash", + "columnName": "iGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iCode", + "columnName": "iCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iApprovalRequiredAfter", + "columnName": "iApprovalRequiredAfter", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iLastModified", + "columnName": "iLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iStored", + "columnName": "iStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iStatus", + "columnName": "iStatus", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iNewUserRole", + "columnName": "iNewUserRole", + "affinity": "INTEGER" + }, + { + "fieldPath": "iNewUserFirstInvite", + "columnName": "iNewUserFirstInvite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iForFamilyOfGuid", + "columnName": "iForFamilyOfGuid", + "affinity": "TEXT" + }, + { + "fieldPath": "iForFamilyOfGuidHash", + "columnName": "iForFamilyOfGuidHash", + "affinity": "INTEGER" + }, + { + "fieldPath": "iForClassGuid", + "columnName": "iForClassGuid", + "affinity": "TEXT" + }, + { + "fieldPath": "iForClassName", + "columnName": "iForClassName", + "affinity": "TEXT" + }, + { + "fieldPath": "iInviteMode", + "columnName": "iInviteMode", + "affinity": "INTEGER" + }, + { + "fieldPath": "iSchoolName", + "columnName": "iSchoolName", + "affinity": "TEXT" + }, + { + "fieldPath": "iForClassGuidHash", + "columnName": "iForClassGuidHash", + "affinity": "INTEGER" + }, + { + "fieldPath": "iForClassRole", + "columnName": "iForClassRole", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "iGuidHash" + ] + } + }, + { + "tableName": "LangMapEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`lmeId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `lmeTopParentType` INTEGER NOT NULL, `lmeTopParentUid1` INTEGER NOT NULL, `lmeTopParentUid2` INTEGER NOT NULL, `lmePropType` INTEGER NOT NULL, `lmePropFk` INTEGER NOT NULL, `lmeLang` TEXT NOT NULL, `lmeRegion` TEXT, `lmeValue` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "lmeId", + "columnName": "lmeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lmeTopParentType", + "columnName": "lmeTopParentType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lmeTopParentUid1", + "columnName": "lmeTopParentUid1", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lmeTopParentUid2", + "columnName": "lmeTopParentUid2", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lmePropType", + "columnName": "lmePropType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lmePropFk", + "columnName": "lmePropFk", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lmeLang", + "columnName": "lmeLang", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lmeRegion", + "columnName": "lmeRegion", + "affinity": "TEXT" + }, + { + "fieldPath": "lmeValue", + "columnName": "lmeValue", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "lmeId" + ] + }, + "indices": [ + { + "name": "index_LangMapEntity_lmeTopParentType_lmeTopParentUid1_lmeTopParentUid2_lmePropType", + "unique": false, + "columnNames": [ + "lmeTopParentType", + "lmeTopParentUid1", + "lmeTopParentUid2", + "lmePropType" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LangMapEntity_lmeTopParentType_lmeTopParentUid1_lmeTopParentUid2_lmePropType` ON `${TABLE_NAME}` (`lmeTopParentType`, `lmeTopParentUid1`, `lmeTopParentUid2`, `lmePropType`)" + } + ] + }, + { + "tableName": "ReadiumLinkEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rleId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `rleOpdsParentType` INTEGER NOT NULL, `rleOpdsParentUid` INTEGER NOT NULL, `rlePropType` TEXT NOT NULL, `rlePropFk` INTEGER NOT NULL, `rleIndex` INTEGER NOT NULL, `rleHref` TEXT NOT NULL, `rleRel` TEXT, `rleType` TEXT, `rleTitle` TEXT, `rleTemplated` INTEGER, `rleProperties` TEXT, `rleHeight` INTEGER, `rleWidth` INTEGER, `rleSize` INTEGER, `rleBitrate` REAL, `rleDuration` REAL, `rleLanguage` TEXT)", + "fields": [ + { + "fieldPath": "rleId", + "columnName": "rleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rleOpdsParentType", + "columnName": "rleOpdsParentType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rleOpdsParentUid", + "columnName": "rleOpdsParentUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rlePropType", + "columnName": "rlePropType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rlePropFk", + "columnName": "rlePropFk", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rleIndex", + "columnName": "rleIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rleHref", + "columnName": "rleHref", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rleRel", + "columnName": "rleRel", + "affinity": "TEXT" + }, + { + "fieldPath": "rleType", + "columnName": "rleType", + "affinity": "TEXT" + }, + { + "fieldPath": "rleTitle", + "columnName": "rleTitle", + "affinity": "TEXT" + }, + { + "fieldPath": "rleTemplated", + "columnName": "rleTemplated", + "affinity": "INTEGER" + }, + { + "fieldPath": "rleProperties", + "columnName": "rleProperties", + "affinity": "TEXT" + }, + { + "fieldPath": "rleHeight", + "columnName": "rleHeight", + "affinity": "INTEGER" + }, + { + "fieldPath": "rleWidth", + "columnName": "rleWidth", + "affinity": "INTEGER" + }, + { + "fieldPath": "rleSize", + "columnName": "rleSize", + "affinity": "INTEGER" + }, + { + "fieldPath": "rleBitrate", + "columnName": "rleBitrate", + "affinity": "REAL" + }, + { + "fieldPath": "rleDuration", + "columnName": "rleDuration", + "affinity": "REAL" + }, + { + "fieldPath": "rleLanguage", + "columnName": "rleLanguage", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "rleId" + ] + } + }, + { + "tableName": "OpdsPublicationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`opeUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `opeOfeUid` INTEGER NOT NULL, `opeOgeUid` INTEGER NOT NULL, `opeIndex` INTEGER NOT NULL, `opeUrl` TEXT, `opeUrlHash` INTEGER NOT NULL, `opeLastModified` INTEGER NOT NULL, `opeEtag` TEXT, `opeMdIdentifier` TEXT, `opeMdLanguage` TEXT, `opeMdType` TEXT, `opeMdDescription` TEXT, `opeMdNumberOfPages` INTEGER, `opeMdDuration` REAL)", + "fields": [ + { + "fieldPath": "opeUid", + "columnName": "opeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "opeOfeUid", + "columnName": "opeOfeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "opeOgeUid", + "columnName": "opeOgeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "opeIndex", + "columnName": "opeIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "opeUrl", + "columnName": "opeUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "opeUrlHash", + "columnName": "opeUrlHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "opeLastModified", + "columnName": "opeLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "opeEtag", + "columnName": "opeEtag", + "affinity": "TEXT" + }, + { + "fieldPath": "opeMdIdentifier", + "columnName": "opeMdIdentifier", + "affinity": "TEXT" + }, + { + "fieldPath": "opeMdLanguage", + "columnName": "opeMdLanguage", + "affinity": "TEXT" + }, + { + "fieldPath": "opeMdType", + "columnName": "opeMdType", + "affinity": "TEXT" + }, + { + "fieldPath": "opeMdDescription", + "columnName": "opeMdDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "opeMdNumberOfPages", + "columnName": "opeMdNumberOfPages", + "affinity": "INTEGER" + }, + { + "fieldPath": "opeMdDuration", + "columnName": "opeMdDuration", + "affinity": "REAL" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "opeUid" + ] + } + }, + { + "tableName": "ReadiumSubjectEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rseUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `rseStringValue` TEXT, `rseTopParentType` INTEGER NOT NULL, `rseTopParentUid` INTEGER NOT NULL, `rseSubjectSortAs` TEXT, `rseSubjectCode` TEXT, `rseSubjectScheme` TEXT, `rseIndex` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "rseUid", + "columnName": "rseUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rseStringValue", + "columnName": "rseStringValue", + "affinity": "TEXT" + }, + { + "fieldPath": "rseTopParentType", + "columnName": "rseTopParentType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rseTopParentUid", + "columnName": "rseTopParentUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rseSubjectSortAs", + "columnName": "rseSubjectSortAs", + "affinity": "TEXT" + }, + { + "fieldPath": "rseSubjectCode", + "columnName": "rseSubjectCode", + "affinity": "TEXT" + }, + { + "fieldPath": "rseSubjectScheme", + "columnName": "rseSubjectScheme", + "affinity": "TEXT" + }, + { + "fieldPath": "rseIndex", + "columnName": "rseIndex", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "rseUid" + ] + } + }, + { + "tableName": "OpdsFacetEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ofaeUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ofaeOfeUid` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "ofaeUid", + "columnName": "ofaeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofaeOfeUid", + "columnName": "ofaeOfeUid", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "ofaeUid" + ] + } + }, + { + "tableName": "OpdsGroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ogeUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ogeOfeUid` INTEGER NOT NULL, `ogeIndex` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "ogeUid", + "columnName": "ogeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ogeOfeUid", + "columnName": "ogeOfeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ogeIndex", + "columnName": "ogeIndex", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "ogeUid" + ] + } + }, + { + "tableName": "OpdsFeedEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ofeUid` INTEGER NOT NULL, `ofeUrl` TEXT NOT NULL, `ofeUrlHash` INTEGER NOT NULL, `ofeLastModified` INTEGER NOT NULL, `ofeLastModifiedHeader` INTEGER NOT NULL, `ofeEtag` TEXT, `ofeStored` INTEGER NOT NULL, PRIMARY KEY(`ofeUid`))", + "fields": [ + { + "fieldPath": "ofeUid", + "columnName": "ofeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofeUrl", + "columnName": "ofeUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ofeUrlHash", + "columnName": "ofeUrlHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofeLastModified", + "columnName": "ofeLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofeLastModifiedHeader", + "columnName": "ofeLastModifiedHeader", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofeEtag", + "columnName": "ofeEtag", + "affinity": "TEXT" + }, + { + "fieldPath": "ofeStored", + "columnName": "ofeStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "ofeUid" + ] + } + }, + { + "tableName": "OpdsFeedMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ofmeUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ofmeOfeUid` INTEGER NOT NULL, `ofmePropType` INTEGER NOT NULL, `ofmePropFk` INTEGER NOT NULL, `ofmeIdentifier` TEXT, `ofmeType` TEXT, `ofmeTitle` TEXT NOT NULL, `ofmeSubtitle` TEXT, `ofmeModified` INTEGER, `ofmeDescription` TEXT, `ofmeItemsPerPage` INTEGER, `ofmeCurrentPage` INTEGER, `ofmeNumberOfItems` INTEGER)", + "fields": [ + { + "fieldPath": "ofmeUid", + "columnName": "ofmeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofmeOfeUid", + "columnName": "ofmeOfeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofmePropType", + "columnName": "ofmePropType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofmePropFk", + "columnName": "ofmePropFk", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofmeIdentifier", + "columnName": "ofmeIdentifier", + "affinity": "TEXT" + }, + { + "fieldPath": "ofmeType", + "columnName": "ofmeType", + "affinity": "TEXT" + }, + { + "fieldPath": "ofmeTitle", + "columnName": "ofmeTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ofmeSubtitle", + "columnName": "ofmeSubtitle", + "affinity": "TEXT" + }, + { + "fieldPath": "ofmeModified", + "columnName": "ofmeModified", + "affinity": "INTEGER" + }, + { + "fieldPath": "ofmeDescription", + "columnName": "ofmeDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "ofmeItemsPerPage", + "columnName": "ofmeItemsPerPage", + "affinity": "INTEGER" + }, + { + "fieldPath": "ofmeCurrentPage", + "columnName": "ofmeCurrentPage", + "affinity": "INTEGER" + }, + { + "fieldPath": "ofmeNumberOfItems", + "columnName": "ofmeNumberOfItems", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "ofmeUid" + ] + } + }, + { + "tableName": "ChangeHistoryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hGuid` TEXT NOT NULL, `hGuidHash` INTEGER NOT NULL, `hTable` TEXT NOT NULL, `hTableGuid` TEXT NOT NULL, `hTimestamp` INTEGER NOT NULL, `hWhoGuid` TEXT NOT NULL, `hWhoGuidHash` INTEGER NOT NULL, PRIMARY KEY(`hGuidHash`))", + "fields": [ + { + "fieldPath": "hGuid", + "columnName": "hGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hGuidHash", + "columnName": "hGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hTable", + "columnName": "hTable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hTableGuid", + "columnName": "hTableGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hTimestamp", + "columnName": "hTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hWhoGuid", + "columnName": "hWhoGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hWhoGuidHash", + "columnName": "hWhoGuidHash", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "hGuidHash" + ] + } + }, + { + "tableName": "ChangeHistoryChangeEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hcId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `hcHistoryGuidHash` INTEGER NOT NULL, `hcField` TEXT NOT NULL, `hcOldVal` TEXT, `hcNewVal` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "hcId", + "columnName": "hcId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hcHistoryGuidHash", + "columnName": "hcHistoryGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hcField", + "columnName": "hcField", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hcOldVal", + "columnName": "hcOldVal", + "affinity": "TEXT" + }, + { + "fieldPath": "hcNewVal", + "columnName": "hcNewVal", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "hcId" + ] + } + } + ], + "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, 'a92de10409147409c00afbdef34ced32')" + ] + } +} \ No newline at end of file diff --git a/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/14.json b/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/14.json new file mode 100644 index 000000000..7f2a082eb --- /dev/null +++ b/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/14.json @@ -0,0 +1,1868 @@ +{ + "formatVersion": 1, + "database": { + "version": 14, + "identityHash": "4cdf3848a741fd847ca58eea4f4c6ec7", + "entities": [ + { + "tableName": "SchoolAppEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`saUid` TEXT NOT NULL, `saUidNum` INTEGER NOT NULL, `saManifestUrl` TEXT NOT NULL, `saStatus` INTEGER NOT NULL, `saLastModified` INTEGER NOT NULL, `saStored` INTEGER NOT NULL, PRIMARY KEY(`saUidNum`))", + "fields": [ + { + "fieldPath": "saUid", + "columnName": "saUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "saUidNum", + "columnName": "saUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "saManifestUrl", + "columnName": "saManifestUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "saStatus", + "columnName": "saStatus", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "saLastModified", + "columnName": "saLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "saStored", + "columnName": "saStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "saUidNum" + ] + } + }, + { + "tableName": "PersonEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pGuid` TEXT NOT NULL, `pGuidHash` INTEGER NOT NULL, `pActive` INTEGER NOT NULL, `pStatus` INTEGER NOT NULL, `pLastModified` INTEGER NOT NULL, `pStored` INTEGER NOT NULL, `pMetadata` TEXT, `pUsername` TEXT, `pGivenName` TEXT NOT NULL, `pFamilyName` TEXT NOT NULL, `pMiddleName` TEXT, `pGender` INTEGER NOT NULL, `pDateOfBirth` INTEGER, `pEmail` TEXT, `pPhoneNumber` TEXT, PRIMARY KEY(`pGuidHash`))", + "fields": [ + { + "fieldPath": "pGuid", + "columnName": "pGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pGuidHash", + "columnName": "pGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pActive", + "columnName": "pActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pStatus", + "columnName": "pStatus", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pLastModified", + "columnName": "pLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pStored", + "columnName": "pStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pMetadata", + "columnName": "pMetadata", + "affinity": "TEXT" + }, + { + "fieldPath": "pUsername", + "columnName": "pUsername", + "affinity": "TEXT" + }, + { + "fieldPath": "pGivenName", + "columnName": "pGivenName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pFamilyName", + "columnName": "pFamilyName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pMiddleName", + "columnName": "pMiddleName", + "affinity": "TEXT" + }, + { + "fieldPath": "pGender", + "columnName": "pGender", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pDateOfBirth", + "columnName": "pDateOfBirth", + "affinity": "INTEGER" + }, + { + "fieldPath": "pEmail", + "columnName": "pEmail", + "affinity": "TEXT" + }, + { + "fieldPath": "pPhoneNumber", + "columnName": "pPhoneNumber", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "pGuidHash" + ] + } + }, + { + "tableName": "PersonRoleEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`prUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `prPersonGuidHash` INTEGER NOT NULL, `prIsPrimaryRole` INTEGER NOT NULL, `prRoleEnum` INTEGER NOT NULL, `prBeginDate` INTEGER, `prEndDate` INTEGER)", + "fields": [ + { + "fieldPath": "prUid", + "columnName": "prUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prPersonGuidHash", + "columnName": "prPersonGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prIsPrimaryRole", + "columnName": "prIsPrimaryRole", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prRoleEnum", + "columnName": "prRoleEnum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prBeginDate", + "columnName": "prBeginDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "prEndDate", + "columnName": "prEndDate", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "prUid" + ] + } + }, + { + "tableName": "PersonRelatedPersonEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`prpUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `prpPersonUidNum` INTEGER NOT NULL, `prpOtherPersonUid` TEXT NOT NULL, `prpOtherPersonUidNum` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "prpUid", + "columnName": "prpUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prpPersonUidNum", + "columnName": "prpPersonUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prpOtherPersonUid", + "columnName": "prpOtherPersonUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "prpOtherPersonUidNum", + "columnName": "prpOtherPersonUidNum", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "prpUid" + ] + } + }, + { + "tableName": "PersonPasswordEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ppwGuidNum` INTEGER NOT NULL, `ppwGuid` TEXT NOT NULL, `authAlgorithm` TEXT NOT NULL, `authEncoded` TEXT NOT NULL, `authSalt` TEXT NOT NULL, `authIterations` INTEGER NOT NULL, `authKeyLen` INTEGER NOT NULL, `ppwLastModified` INTEGER NOT NULL, `ppwStored` INTEGER NOT NULL, PRIMARY KEY(`ppwGuidNum`))", + "fields": [ + { + "fieldPath": "ppwGuidNum", + "columnName": "ppwGuidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppwGuid", + "columnName": "ppwGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authAlgorithm", + "columnName": "authAlgorithm", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authEncoded", + "columnName": "authEncoded", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authSalt", + "columnName": "authSalt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authIterations", + "columnName": "authIterations", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authKeyLen", + "columnName": "authKeyLen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppwLastModified", + "columnName": "ppwLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppwStored", + "columnName": "ppwStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "ppwGuidNum" + ] + } + }, + { + "tableName": "PersonPasskeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ppPersonUidNum` INTEGER NOT NULL, `ppCredentialId` TEXT NOT NULL, `ppLastModified` INTEGER NOT NULL, `ppStored` INTEGER NOT NULL, `ppAttestationObj` TEXT, `ppClientDataJson` TEXT, `ppOriginString` TEXT, `ppChallengeString` TEXT, `ppPublicKey` TEXT, `isRevoked` INTEGER NOT NULL, `ppDeviceName` TEXT NOT NULL DEFAULT '', `ppTimeCreated` INTEGER NOT NULL DEFAULT 0, `ppAaguid` TEXT NOT NULL DEFAULT '', `ppProviderName` TEXT NOT NULL DEFAULT '', `ppIconLight` TEXT NOT NULL DEFAULT '', `ppIconDark` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`ppPersonUidNum`, `ppCredentialId`))", + "fields": [ + { + "fieldPath": "ppPersonUidNum", + "columnName": "ppPersonUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppCredentialId", + "columnName": "ppCredentialId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ppLastModified", + "columnName": "ppLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppStored", + "columnName": "ppStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppAttestationObj", + "columnName": "ppAttestationObj", + "affinity": "TEXT" + }, + { + "fieldPath": "ppClientDataJson", + "columnName": "ppClientDataJson", + "affinity": "TEXT" + }, + { + "fieldPath": "ppOriginString", + "columnName": "ppOriginString", + "affinity": "TEXT" + }, + { + "fieldPath": "ppChallengeString", + "columnName": "ppChallengeString", + "affinity": "TEXT" + }, + { + "fieldPath": "ppPublicKey", + "columnName": "ppPublicKey", + "affinity": "TEXT" + }, + { + "fieldPath": "isRevoked", + "columnName": "isRevoked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ppDeviceName", + "columnName": "ppDeviceName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "ppTimeCreated", + "columnName": "ppTimeCreated", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ppAaguid", + "columnName": "ppAaguid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "ppProviderName", + "columnName": "ppProviderName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "ppIconLight", + "columnName": "ppIconLight", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "ppIconDark", + "columnName": "ppIconDark", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "ppPersonUidNum", + "ppCredentialId" + ] + } + }, + { + "tableName": "AuthTokenEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`atUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `atPGuidHash` INTEGER NOT NULL, `atPGuid` TEXT NOT NULL, `atCode` TEXT, `atToken` TEXT NOT NULL, `atTimeCreated` INTEGER NOT NULL, `atTtl` INTEGER NOT NULL, `atPlatform` TEXT, `atAndroidSdkInt` INTEGER, `atVersion` TEXT, `atManufacturer` TEXT, `atModel` TEXT, `atRam` INTEGER)", + "fields": [ + { + "fieldPath": "atUid", + "columnName": "atUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "atPGuidHash", + "columnName": "atPGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "atPGuid", + "columnName": "atPGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "atCode", + "columnName": "atCode", + "affinity": "TEXT" + }, + { + "fieldPath": "atToken", + "columnName": "atToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "atTimeCreated", + "columnName": "atTimeCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "atTtl", + "columnName": "atTtl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "atPlatform", + "columnName": "atPlatform", + "affinity": "TEXT" + }, + { + "fieldPath": "atAndroidSdkInt", + "columnName": "atAndroidSdkInt", + "affinity": "INTEGER" + }, + { + "fieldPath": "atVersion", + "columnName": "atVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "atManufacturer", + "columnName": "atManufacturer", + "affinity": "TEXT" + }, + { + "fieldPath": "atModel", + "columnName": "atModel", + "affinity": "TEXT" + }, + { + "fieldPath": "atRam", + "columnName": "atRam", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "atUid" + ] + } + }, + { + "tableName": "ReportEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rGuid` TEXT NOT NULL, `rOwnerGuid` TEXT NOT NULL, `rTitle` TEXT NOT NULL, `rOptions` TEXT NOT NULL, `rIsTemplate` INTEGER NOT NULL, `rActive` INTEGER NOT NULL, `rLastModified` INTEGER NOT NULL, `rStored` INTEGER NOT NULL, PRIMARY KEY(`rGuid`))", + "fields": [ + { + "fieldPath": "rGuid", + "columnName": "rGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rOwnerGuid", + "columnName": "rOwnerGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rTitle", + "columnName": "rTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rOptions", + "columnName": "rOptions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rIsTemplate", + "columnName": "rIsTemplate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rActive", + "columnName": "rActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rLastModified", + "columnName": "rLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rStored", + "columnName": "rStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "rGuid" + ] + } + }, + { + "tableName": "IndicatorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`indicatorId` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, `type` TEXT NOT NULL, `sql` TEXT NOT NULL, PRIMARY KEY(`indicatorId`))", + "fields": [ + { + "fieldPath": "indicatorId", + "columnName": "indicatorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sql", + "columnName": "sql", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "indicatorId" + ] + } + }, + { + "tableName": "ClassEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cGuid` TEXT NOT NULL, `cGuidHash` INTEGER NOT NULL, `cTitle` TEXT NOT NULL, `cStatus` INTEGER NOT NULL, `cDescription` TEXT, `cLastModified` INTEGER NOT NULL, `cStored` INTEGER NOT NULL, `cTeacherInviteGuid` TEXT, `cStudentInviteGuid` TEXT, PRIMARY KEY(`cGuidHash`))", + "fields": [ + { + "fieldPath": "cGuid", + "columnName": "cGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cGuidHash", + "columnName": "cGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cTitle", + "columnName": "cTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cStatus", + "columnName": "cStatus", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cDescription", + "columnName": "cDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "cLastModified", + "columnName": "cLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cStored", + "columnName": "cStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cTeacherInviteGuid", + "columnName": "cTeacherInviteGuid", + "affinity": "TEXT" + }, + { + "fieldPath": "cStudentInviteGuid", + "columnName": "cStudentInviteGuid", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "cGuidHash" + ] + } + }, + { + "tableName": "ClassPermissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cpeId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `cpeClassUidNum` INTEGER NOT NULL, `cpeToEnrollmentRole` INTEGER, `cpePermissions` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "cpeId", + "columnName": "cpeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cpeClassUidNum", + "columnName": "cpeClassUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cpeToEnrollmentRole", + "columnName": "cpeToEnrollmentRole", + "affinity": "INTEGER" + }, + { + "fieldPath": "cpePermissions", + "columnName": "cpePermissions", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "cpeId" + ] + } + }, + { + "tableName": "EnrollmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eUid` TEXT NOT NULL, `eUidNum` INTEGER NOT NULL, `eStatus` INTEGER NOT NULL, `eLastModified` INTEGER NOT NULL, `eStored` INTEGER NOT NULL, `eMetadata` TEXT, `eClassUid` TEXT NOT NULL, `eClassUidNum` INTEGER NOT NULL, `ePersonUid` TEXT NOT NULL, `ePersonUidNum` INTEGER NOT NULL, `eRole` INTEGER NOT NULL, `eBeginDate` INTEGER, `eEndDate` INTEGER, `eRemovedAt` INTEGER, `eInviteCode` TEXT, `eApprovedByPersonUidNum` INTEGER NOT NULL, `eApprovedByPersonUid` TEXT, PRIMARY KEY(`eUidNum`))", + "fields": [ + { + "fieldPath": "eUid", + "columnName": "eUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eUidNum", + "columnName": "eUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eStatus", + "columnName": "eStatus", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eLastModified", + "columnName": "eLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eStored", + "columnName": "eStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eMetadata", + "columnName": "eMetadata", + "affinity": "TEXT" + }, + { + "fieldPath": "eClassUid", + "columnName": "eClassUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eClassUidNum", + "columnName": "eClassUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ePersonUid", + "columnName": "ePersonUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ePersonUidNum", + "columnName": "ePersonUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eRole", + "columnName": "eRole", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eBeginDate", + "columnName": "eBeginDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "eEndDate", + "columnName": "eEndDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "eRemovedAt", + "columnName": "eRemovedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "eInviteCode", + "columnName": "eInviteCode", + "affinity": "TEXT" + }, + { + "fieldPath": "eApprovedByPersonUidNum", + "columnName": "eApprovedByPersonUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eApprovedByPersonUid", + "columnName": "eApprovedByPersonUid", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "eUidNum" + ] + } + }, + { + "tableName": "AssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`aeUid` TEXT NOT NULL, `aeUidNum` INTEGER NOT NULL, `aeTitle` TEXT NOT NULL, `aeDescription` TEXT NOT NULL, `aeClassUid` TEXT NOT NULL, `aeClassUidNum` INTEGER NOT NULL, `aeDeadline` INTEGER, `aeLastModified` INTEGER NOT NULL, `aeStored` INTEGER NOT NULL, PRIMARY KEY(`aeUidNum`))", + "fields": [ + { + "fieldPath": "aeUid", + "columnName": "aeUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "aeUidNum", + "columnName": "aeUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "aeTitle", + "columnName": "aeTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "aeDescription", + "columnName": "aeDescription", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "aeClassUid", + "columnName": "aeClassUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "aeClassUidNum", + "columnName": "aeClassUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "aeDeadline", + "columnName": "aeDeadline", + "affinity": "INTEGER" + }, + { + "fieldPath": "aeLastModified", + "columnName": "aeLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "aeStored", + "columnName": "aeStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "aeUidNum" + ] + } + }, + { + "tableName": "AssignmentLearningResourceRefEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`alrrAeUidNum` INTEGER NOT NULL, `alrrLearningUnitManifestUrlHash` INTEGER NOT NULL, `alrrLearningUnitManifestUrl` TEXT NOT NULL, `alrrAppManifestUrl` TEXT NOT NULL, PRIMARY KEY(`alrrAeUidNum`, `alrrLearningUnitManifestUrlHash`))", + "fields": [ + { + "fieldPath": "alrrAeUidNum", + "columnName": "alrrAeUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alrrLearningUnitManifestUrlHash", + "columnName": "alrrLearningUnitManifestUrlHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alrrLearningUnitManifestUrl", + "columnName": "alrrLearningUnitManifestUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alrrAppManifestUrl", + "columnName": "alrrAppManifestUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "alrrAeUidNum", + "alrrLearningUnitManifestUrlHash" + ] + } + }, + { + "tableName": "WriteQueueItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`wqiQueueItemId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `wqiModel` INTEGER NOT NULL, `wqiUid` TEXT NOT NULL, `wqiTimeQueued` INTEGER NOT NULL, `wqiAttemptCount` INTEGER NOT NULL, `wqiTimeWritten` INTEGER NOT NULL, `wqiAccountGuid` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "wqiQueueItemId", + "columnName": "wqiQueueItemId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wqiModel", + "columnName": "wqiModel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wqiUid", + "columnName": "wqiUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wqiTimeQueued", + "columnName": "wqiTimeQueued", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wqiAttemptCount", + "columnName": "wqiAttemptCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wqiTimeWritten", + "columnName": "wqiTimeWritten", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wqiAccountGuid", + "columnName": "wqiAccountGuid", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "wqiQueueItemId" + ] + } + }, + { + "tableName": "SchoolPermissionGrantEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`spgUid` TEXT NOT NULL, `spgUidNum` INTEGER NOT NULL, `spgStatusEnum` INTEGER NOT NULL, `spgToRole` INTEGER NOT NULL, `spgPermissions` INTEGER NOT NULL, `spgLastModified` INTEGER NOT NULL, `spgStored` INTEGER NOT NULL, PRIMARY KEY(`spgUidNum`))", + "fields": [ + { + "fieldPath": "spgUid", + "columnName": "spgUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "spgUidNum", + "columnName": "spgUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spgStatusEnum", + "columnName": "spgStatusEnum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spgToRole", + "columnName": "spgToRole", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spgPermissions", + "columnName": "spgPermissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spgLastModified", + "columnName": "spgLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spgStored", + "columnName": "spgStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "spgUidNum" + ] + } + }, + { + "tableName": "PullSyncStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pssAccountPersonUid` TEXT NOT NULL, `pssAccountPersonUidNum` INTEGER NOT NULL, `pssTableId` INTEGER NOT NULL, `pssLastConsistentThrough` INTEGER NOT NULL, `pssPermissionsLastModified` INTEGER NOT NULL, PRIMARY KEY(`pssAccountPersonUid`, `pssTableId`))", + "fields": [ + { + "fieldPath": "pssAccountPersonUid", + "columnName": "pssAccountPersonUid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pssAccountPersonUidNum", + "columnName": "pssAccountPersonUidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pssTableId", + "columnName": "pssTableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pssLastConsistentThrough", + "columnName": "pssLastConsistentThrough", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pssPermissionsLastModified", + "columnName": "pssPermissionsLastModified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "pssAccountPersonUid", + "pssTableId" + ] + } + }, + { + "tableName": "PersonQrBadgeEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pqrGuidNum` INTEGER NOT NULL, `pqrGuid` TEXT NOT NULL, `pqrLastModified` INTEGER NOT NULL, `pqrStored` INTEGER NOT NULL, `pqrQrCodeUrl` TEXT, `pqrStatus` INTEGER NOT NULL, PRIMARY KEY(`pqrGuidNum`))", + "fields": [ + { + "fieldPath": "pqrGuidNum", + "columnName": "pqrGuidNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pqrGuid", + "columnName": "pqrGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pqrLastModified", + "columnName": "pqrLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pqrStored", + "columnName": "pqrStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pqrQrCodeUrl", + "columnName": "pqrQrCodeUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "pqrStatus", + "columnName": "pqrStatus", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "pqrGuidNum" + ] + }, + "indices": [ + { + "name": "index_PersonQrBadgeEntity_pqrQrCodeUrl", + "unique": false, + "columnNames": [ + "pqrQrCodeUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PersonQrBadgeEntity_pqrQrCodeUrl` ON `${TABLE_NAME}` (`pqrQrCodeUrl`)" + } + ] + }, + { + "tableName": "InviteEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`iGuid` TEXT NOT NULL, `iGuidHash` INTEGER NOT NULL, `iCode` TEXT NOT NULL, `iApprovalRequiredAfter` INTEGER NOT NULL, `iLastModified` INTEGER NOT NULL, `iStored` INTEGER NOT NULL, `iStatus` INTEGER NOT NULL, `iNewUserRole` INTEGER, `iNewUserFirstInvite` INTEGER NOT NULL, `iForFamilyOfGuid` TEXT, `iForFamilyOfGuidHash` INTEGER, `iForClassGuid` TEXT, `iForClassName` TEXT, `iInviteMode` INTEGER, `iSchoolName` TEXT, `iForClassGuidHash` INTEGER, `iForClassRole` INTEGER, PRIMARY KEY(`iGuidHash`))", + "fields": [ + { + "fieldPath": "iGuid", + "columnName": "iGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iGuidHash", + "columnName": "iGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iCode", + "columnName": "iCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iApprovalRequiredAfter", + "columnName": "iApprovalRequiredAfter", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iLastModified", + "columnName": "iLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iStored", + "columnName": "iStored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iStatus", + "columnName": "iStatus", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iNewUserRole", + "columnName": "iNewUserRole", + "affinity": "INTEGER" + }, + { + "fieldPath": "iNewUserFirstInvite", + "columnName": "iNewUserFirstInvite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iForFamilyOfGuid", + "columnName": "iForFamilyOfGuid", + "affinity": "TEXT" + }, + { + "fieldPath": "iForFamilyOfGuidHash", + "columnName": "iForFamilyOfGuidHash", + "affinity": "INTEGER" + }, + { + "fieldPath": "iForClassGuid", + "columnName": "iForClassGuid", + "affinity": "TEXT" + }, + { + "fieldPath": "iForClassName", + "columnName": "iForClassName", + "affinity": "TEXT" + }, + { + "fieldPath": "iInviteMode", + "columnName": "iInviteMode", + "affinity": "INTEGER" + }, + { + "fieldPath": "iSchoolName", + "columnName": "iSchoolName", + "affinity": "TEXT" + }, + { + "fieldPath": "iForClassGuidHash", + "columnName": "iForClassGuidHash", + "affinity": "INTEGER" + }, + { + "fieldPath": "iForClassRole", + "columnName": "iForClassRole", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "iGuidHash" + ] + } + }, + { + "tableName": "LangMapEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`lmeId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `lmeTopParentType` INTEGER NOT NULL, `lmeTopParentUid1` INTEGER NOT NULL, `lmeTopParentUid2` INTEGER NOT NULL, `lmePropType` INTEGER NOT NULL, `lmePropFk` INTEGER NOT NULL, `lmeLang` TEXT NOT NULL, `lmeRegion` TEXT, `lmeValue` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "lmeId", + "columnName": "lmeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lmeTopParentType", + "columnName": "lmeTopParentType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lmeTopParentUid1", + "columnName": "lmeTopParentUid1", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lmeTopParentUid2", + "columnName": "lmeTopParentUid2", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lmePropType", + "columnName": "lmePropType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lmePropFk", + "columnName": "lmePropFk", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lmeLang", + "columnName": "lmeLang", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lmeRegion", + "columnName": "lmeRegion", + "affinity": "TEXT" + }, + { + "fieldPath": "lmeValue", + "columnName": "lmeValue", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "lmeId" + ] + }, + "indices": [ + { + "name": "index_LangMapEntity_lmeTopParentType_lmeTopParentUid1_lmeTopParentUid2_lmePropType", + "unique": false, + "columnNames": [ + "lmeTopParentType", + "lmeTopParentUid1", + "lmeTopParentUid2", + "lmePropType" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LangMapEntity_lmeTopParentType_lmeTopParentUid1_lmeTopParentUid2_lmePropType` ON `${TABLE_NAME}` (`lmeTopParentType`, `lmeTopParentUid1`, `lmeTopParentUid2`, `lmePropType`)" + } + ] + }, + { + "tableName": "ReadiumLinkEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rleId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `rleOpdsParentType` INTEGER NOT NULL, `rleOpdsParentUid` INTEGER NOT NULL, `rlePropType` TEXT NOT NULL, `rlePropFk` INTEGER NOT NULL, `rleIndex` INTEGER NOT NULL, `rleHref` TEXT NOT NULL, `rleRel` TEXT, `rleType` TEXT, `rleTitle` TEXT, `rleTemplated` INTEGER, `rleProperties` TEXT, `rleHeight` INTEGER, `rleWidth` INTEGER, `rleSize` INTEGER, `rleBitrate` REAL, `rleDuration` REAL, `rleLanguage` TEXT)", + "fields": [ + { + "fieldPath": "rleId", + "columnName": "rleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rleOpdsParentType", + "columnName": "rleOpdsParentType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rleOpdsParentUid", + "columnName": "rleOpdsParentUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rlePropType", + "columnName": "rlePropType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rlePropFk", + "columnName": "rlePropFk", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rleIndex", + "columnName": "rleIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rleHref", + "columnName": "rleHref", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rleRel", + "columnName": "rleRel", + "affinity": "TEXT" + }, + { + "fieldPath": "rleType", + "columnName": "rleType", + "affinity": "TEXT" + }, + { + "fieldPath": "rleTitle", + "columnName": "rleTitle", + "affinity": "TEXT" + }, + { + "fieldPath": "rleTemplated", + "columnName": "rleTemplated", + "affinity": "INTEGER" + }, + { + "fieldPath": "rleProperties", + "columnName": "rleProperties", + "affinity": "TEXT" + }, + { + "fieldPath": "rleHeight", + "columnName": "rleHeight", + "affinity": "INTEGER" + }, + { + "fieldPath": "rleWidth", + "columnName": "rleWidth", + "affinity": "INTEGER" + }, + { + "fieldPath": "rleSize", + "columnName": "rleSize", + "affinity": "INTEGER" + }, + { + "fieldPath": "rleBitrate", + "columnName": "rleBitrate", + "affinity": "REAL" + }, + { + "fieldPath": "rleDuration", + "columnName": "rleDuration", + "affinity": "REAL" + }, + { + "fieldPath": "rleLanguage", + "columnName": "rleLanguage", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "rleId" + ] + } + }, + { + "tableName": "OpdsPublicationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`opeUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `opeOfeUid` INTEGER NOT NULL, `opeOgeUid` INTEGER NOT NULL, `opeIndex` INTEGER NOT NULL, `opeUrl` TEXT, `opeUrlHash` INTEGER NOT NULL, `opeLastModified` INTEGER NOT NULL, `opeEtag` TEXT, `opeMdIdentifier` TEXT, `opeMdLanguage` TEXT, `opeMdType` TEXT, `opeMdDescription` TEXT, `opeMdNumberOfPages` INTEGER, `opeMdDuration` REAL)", + "fields": [ + { + "fieldPath": "opeUid", + "columnName": "opeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "opeOfeUid", + "columnName": "opeOfeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "opeOgeUid", + "columnName": "opeOgeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "opeIndex", + "columnName": "opeIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "opeUrl", + "columnName": "opeUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "opeUrlHash", + "columnName": "opeUrlHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "opeLastModified", + "columnName": "opeLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "opeEtag", + "columnName": "opeEtag", + "affinity": "TEXT" + }, + { + "fieldPath": "opeMdIdentifier", + "columnName": "opeMdIdentifier", + "affinity": "TEXT" + }, + { + "fieldPath": "opeMdLanguage", + "columnName": "opeMdLanguage", + "affinity": "TEXT" + }, + { + "fieldPath": "opeMdType", + "columnName": "opeMdType", + "affinity": "TEXT" + }, + { + "fieldPath": "opeMdDescription", + "columnName": "opeMdDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "opeMdNumberOfPages", + "columnName": "opeMdNumberOfPages", + "affinity": "INTEGER" + }, + { + "fieldPath": "opeMdDuration", + "columnName": "opeMdDuration", + "affinity": "REAL" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "opeUid" + ] + } + }, + { + "tableName": "ReadiumSubjectEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rseUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `rseStringValue` TEXT, `rseTopParentType` INTEGER NOT NULL, `rseTopParentUid` INTEGER NOT NULL, `rseSubjectSortAs` TEXT, `rseSubjectCode` TEXT, `rseSubjectScheme` TEXT, `rseIndex` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "rseUid", + "columnName": "rseUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rseStringValue", + "columnName": "rseStringValue", + "affinity": "TEXT" + }, + { + "fieldPath": "rseTopParentType", + "columnName": "rseTopParentType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rseTopParentUid", + "columnName": "rseTopParentUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rseSubjectSortAs", + "columnName": "rseSubjectSortAs", + "affinity": "TEXT" + }, + { + "fieldPath": "rseSubjectCode", + "columnName": "rseSubjectCode", + "affinity": "TEXT" + }, + { + "fieldPath": "rseSubjectScheme", + "columnName": "rseSubjectScheme", + "affinity": "TEXT" + }, + { + "fieldPath": "rseIndex", + "columnName": "rseIndex", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "rseUid" + ] + } + }, + { + "tableName": "OpdsFacetEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ofaeUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ofaeOfeUid` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "ofaeUid", + "columnName": "ofaeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofaeOfeUid", + "columnName": "ofaeOfeUid", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "ofaeUid" + ] + } + }, + { + "tableName": "OpdsGroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ogeUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ogeOfeUid` INTEGER NOT NULL, `ogeIndex` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "ogeUid", + "columnName": "ogeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ogeOfeUid", + "columnName": "ogeOfeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ogeIndex", + "columnName": "ogeIndex", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "ogeUid" + ] + } + }, + { + "tableName": "OpdsFeedEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ofeUid` INTEGER NOT NULL, `ofeUrl` TEXT NOT NULL, `ofeUrlHash` INTEGER NOT NULL, `ofeLastModified` INTEGER NOT NULL, `ofeLastModifiedHeader` INTEGER NOT NULL, `ofeEtag` TEXT, `ofeStored` INTEGER NOT NULL, PRIMARY KEY(`ofeUid`))", + "fields": [ + { + "fieldPath": "ofeUid", + "columnName": "ofeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofeUrl", + "columnName": "ofeUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ofeUrlHash", + "columnName": "ofeUrlHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofeLastModified", + "columnName": "ofeLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofeLastModifiedHeader", + "columnName": "ofeLastModifiedHeader", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofeEtag", + "columnName": "ofeEtag", + "affinity": "TEXT" + }, + { + "fieldPath": "ofeStored", + "columnName": "ofeStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "ofeUid" + ] + } + }, + { + "tableName": "OpdsFeedMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ofmeUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ofmeOfeUid` INTEGER NOT NULL, `ofmePropType` INTEGER NOT NULL, `ofmePropFk` INTEGER NOT NULL, `ofmeIdentifier` TEXT, `ofmeType` TEXT, `ofmeTitle` TEXT NOT NULL, `ofmeSubtitle` TEXT, `ofmeModified` INTEGER, `ofmeDescription` TEXT, `ofmeItemsPerPage` INTEGER, `ofmeCurrentPage` INTEGER, `ofmeNumberOfItems` INTEGER)", + "fields": [ + { + "fieldPath": "ofmeUid", + "columnName": "ofmeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofmeOfeUid", + "columnName": "ofmeOfeUid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofmePropType", + "columnName": "ofmePropType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofmePropFk", + "columnName": "ofmePropFk", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ofmeIdentifier", + "columnName": "ofmeIdentifier", + "affinity": "TEXT" + }, + { + "fieldPath": "ofmeType", + "columnName": "ofmeType", + "affinity": "TEXT" + }, + { + "fieldPath": "ofmeTitle", + "columnName": "ofmeTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ofmeSubtitle", + "columnName": "ofmeSubtitle", + "affinity": "TEXT" + }, + { + "fieldPath": "ofmeModified", + "columnName": "ofmeModified", + "affinity": "INTEGER" + }, + { + "fieldPath": "ofmeDescription", + "columnName": "ofmeDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "ofmeItemsPerPage", + "columnName": "ofmeItemsPerPage", + "affinity": "INTEGER" + }, + { + "fieldPath": "ofmeCurrentPage", + "columnName": "ofmeCurrentPage", + "affinity": "INTEGER" + }, + { + "fieldPath": "ofmeNumberOfItems", + "columnName": "ofmeNumberOfItems", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "ofmeUid" + ] + } + }, + { + "tableName": "ChangeHistoryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hGuid` TEXT NOT NULL, `hGuidHash` INTEGER NOT NULL, `hTable` TEXT NOT NULL, `hTableGuid` TEXT NOT NULL, `hTimestamp` INTEGER NOT NULL, `hWhoGuid` TEXT NOT NULL, `hWhoGuidHash` INTEGER NOT NULL, `hLastModified` INTEGER NOT NULL, `hStored` INTEGER NOT NULL, PRIMARY KEY(`hGuidHash`))", + "fields": [ + { + "fieldPath": "hGuid", + "columnName": "hGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hGuidHash", + "columnName": "hGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hTable", + "columnName": "hTable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hTableGuid", + "columnName": "hTableGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hTimestamp", + "columnName": "hTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hWhoGuid", + "columnName": "hWhoGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hWhoGuidHash", + "columnName": "hWhoGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hLastModified", + "columnName": "hLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hStored", + "columnName": "hStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "hGuidHash" + ] + } + }, + { + "tableName": "ChangeHistoryChangeEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hcId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `hcHistoryGuidHash` INTEGER NOT NULL, `hcField` TEXT NOT NULL, `hcOldVal` TEXT, `hcNewVal` TEXT NOT NULL, `hcSynced` INTEGER NOT NULL, `hcLastModified` INTEGER NOT NULL, `hcStored` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "hcId", + "columnName": "hcId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hcHistoryGuidHash", + "columnName": "hcHistoryGuidHash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hcField", + "columnName": "hcField", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hcOldVal", + "columnName": "hcOldVal", + "affinity": "TEXT" + }, + { + "fieldPath": "hcNewVal", + "columnName": "hcNewVal", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hcSynced", + "columnName": "hcSynced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hcLastModified", + "columnName": "hcLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hcStored", + "columnName": "hcStored", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "hcId" + ] + } + } + ], + "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, '4cdf3848a741fd847ca58eea4f4c6ec7')" + ] + } +} \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabase.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabase.kt index abe2d00ad..233a35bab 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabase.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabase.kt @@ -64,6 +64,7 @@ import world.respect.datalayer.db.school.entities.ClassPermissionEntity import world.respect.datalayer.db.school.entities.PullSyncStatusEntity import world.respect.datalayer.db.school.entities.SchoolPermissionGrantEntity import world.respect.datalayer.school.model.Assignment +import world.respect.datalayer.school.model.ChangeHistoryEntry import world.respect.datalayer.school.model.Clazz import world.respect.datalayer.school.model.Enrollment import world.respect.datalayer.school.model.Indicator @@ -111,7 +112,7 @@ import world.respect.datalayer.school.model.Report ChangeHistoryEntity::class, ChangeHistoryChangeEntity::class, ], - version = 12, + version = 14, ) @TypeConverters(SharedConverters::class, SchoolTypeConverters::class, OpdsTypeConverters::class) @ConstructedBy(RespectSchoolDatabaseConstructor::class) @@ -186,6 +187,7 @@ abstract class RespectSchoolDatabase: RoomDatabase() { OpdsFacetEntity.TABLE_ID, OpdsGroupEntity.TABLE_ID, OpdsFeedEntity.TABLE_ID, + ChangeHistoryEntry.TABLE_ID, ) } diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabaseMigrations.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabaseMigrations.kt index 37285954d..8b4d6f717 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabaseMigrations.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabaseMigrations.kt @@ -31,6 +31,8 @@ val MIGRATE_12_13 = object : Migration(12, 13) { `hTimestamp` INTEGER NOT NULL, `hWhoGuid` TEXT NOT NULL, `hWhoGuidHash` INTEGER NOT NULL, + `hLastModified` INTEGER NOT NULL, + `hStored` INTEGER NOT NULL, `hTableGuid` TEXT NOT NULL, PRIMARY KEY(`hGuidHash`) ) @@ -42,16 +44,26 @@ val MIGRATE_12_13 = object : Migration(12, 13) { `hcHistoryGuidHash` INTEGER NOT NULL, `hcField` TEXT NOT NULL, `hcOldVal` TEXT, + `hcLastModified` INTEGER NOT NULL, + `hcStored` INTEGER NOT NULL, `hcNewVal` TEXT NOT NULL ) """.trimIndent()) } } +val MIGRATE_13_14 = object : Migration(13, 14) { + override fun migrate(connection: SQLiteConnection) { + connection.execSQL(""" + ALTER TABLE `ChangeHistoryEntity` ADD COLUMN `hSynced` INTEGER NOT NULL DEFAULT 0 + """.trimIndent()) + + } +} fun RoomDatabase.Builder.addCommonMigrations( ): RoomDatabase.Builder { - return this.addMigrations(MIGRATION_11_12,MIGRATE_12_13) + return this.addMigrations(MIGRATION_11_12,MIGRATE_12_13,MIGRATE_13_14) } diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt index 778afa9e6..79166d335 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt @@ -79,7 +79,14 @@ class SchoolDataSourceDb( } override val personDataSource: PersonDataSourceLocal by lazy { - PersonDataSourceDb(schoolDb, uidNumberMapper, authenticatedUser, checkPersonPermissionUseCase,changeHistoryDataSource) + PersonDataSourceDb( + schoolDb, + uidNumberMapper, + authenticatedUser, + checkPersonPermissionUseCase, + changeHistoryDataSource, + primaryKeyGenerator + ) } override val personPasskeyDataSource: PersonPasskeyDataSourceLocal by lazy { @@ -104,7 +111,7 @@ class SchoolDataSourceDb( } override val classDataSource: ClassDataSourceLocal by lazy { - ClassDatasourceDb(schoolDb, uidNumberMapper, authenticatedUser) + ClassDatasourceDb(schoolDb, uidNumberMapper, authenticatedUser,changeHistoryDataSource) } override val inviteDataSource: InviteDataSourceLocal by lazy { diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt index 71738e716..988226595 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt @@ -38,17 +38,31 @@ class ChangeHistoryDataSourceDb( } override fun findByGuidAsFlow( - loadParams: DataLoadParams, guid: String - ): Flow> { + ): Flow>> { return schoolDb.getChangeHistoryDao() - .findByGuidAsFlow(guid).map { - it?.let { - DataReadyState(it.toModel()) - } ?: NoDataLoadedState.notFound() + .findByGuidAsFlow(guid) + .map { list -> + if (list.isNotEmpty()) { + DataReadyState(list.map { it.toModel() }) + } else { + NoDataLoadedState.notFound() + } } } + override suspend fun list( + loadParams: DataLoadParams, + params: ChangeHistoryDataSource.GetListParams + ): DataLoadState> { + val result = schoolDb.getChangeHistoryDao().findByGuid(params.common.guid?:"") + return if (!result.isNullOrEmpty()) { + DataReadyState(result.map { it.toModel() }) + } else { + NoDataLoadedState.notFound() + } + } + override fun listAsPagingSource( dataLoadParams: DataLoadParams, getListParams: ChangeHistoryDataSource.GetListParams @@ -63,6 +77,19 @@ class ChangeHistoryDataSourceDb( } } + override suspend fun markSentToServer( + changeHistoryEntries: List + ) { + if (changeHistoryEntries.isEmpty()) return + + val historyGuidHashes = changeHistoryEntries + .map { uidNumberMapper(it.guid) } + + if (historyGuidHashes.isEmpty()) return + + schoolDb.getChangeHistoryDao().markByHistoryGuids(historyGuidHashes) + } + override suspend fun store(list: List) { if(list.isEmpty()) return @@ -70,9 +97,10 @@ class ChangeHistoryDataSourceDb( schoolDb.useWriterConnection { con -> con.withTransaction(Transactor.SQLiteTransactionType.IMMEDIATE) { list.forEach { + val entities = it.toEntities(uidNumberMapper) schoolDb.getChangeHistoryDao().insertHistoryWithChanges( - history = it.toEntities(uidNumberMapper).changeHistoryEntity, - changes = it.toEntities(uidNumberMapper).changeEntities + history = entities.changeHistoryEntity, + changes = entities.changeEntities ) } } diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryMarkSentToServerDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryMarkSentToServerDb.kt new file mode 100644 index 000000000..b638c2c72 --- /dev/null +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryMarkSentToServerDb.kt @@ -0,0 +1,24 @@ +package world.respect.datalayer.db.school + +import world.respect.datalayer.ChangeHistoryMarkSentToServer +import world.respect.datalayer.UidNumberMapper +import world.respect.datalayer.db.RespectSchoolDatabase +import world.respect.datalayer.school.model.ChangeHistoryEntry + +class ChangeHistoryMarkSentToServerDb( + private val schoolDb: RespectSchoolDatabase, + private val uidNumberMapper: UidNumberMapper +) : ChangeHistoryMarkSentToServer { + override suspend fun markSentToServer(changeHistoryEntry: List) { + + if (changeHistoryEntry.isEmpty()) return + + val historyGuidHashes = changeHistoryEntry + .map { uidNumberMapper(it.guid) } + + if (historyGuidHashes.isEmpty()) return + + schoolDb.getChangeHistoryDao().markByHistoryGuids(historyGuidHashes) } + + +} \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryProviderDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryProviderDb.kt new file mode 100644 index 000000000..28c56cfaf --- /dev/null +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryProviderDb.kt @@ -0,0 +1,24 @@ +package world.respect.datalayer.db.school + +import world.respect.datalayer.ChangeHistoryProvider +import world.respect.datalayer.db.RespectSchoolDatabase +import world.respect.datalayer.db.school.adapters.toModel +import world.respect.datalayer.school.model.ChangeHistoryEntry +import world.respect.datalayer.school.model.ChangeHistoryTableEnum + +class ChangeHistoryProviderDb( + private val schoolDb: RespectSchoolDatabase, +) : ChangeHistoryProvider { + + override suspend fun getChangeHistoryEntries( + tableId: ChangeHistoryTableEnum, + uids: List + ): List { + + return schoolDb.getChangeHistoryDao().getByTableAndUids( + tableEnum = tableId, + uids = uids + ).map { it.toModel() } + } +} + diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ClassDatasourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ClassDatasourceDb.kt index 1051c75fd..f3beea151 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ClassDatasourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ClassDatasourceDb.kt @@ -12,17 +12,19 @@ import world.respect.datalayer.DataReadyState import world.respect.datalayer.NoDataLoadedState import world.respect.datalayer.UidNumberMapper import world.respect.datalayer.db.RespectSchoolDatabase +import world.respect.datalayer.db.school.adapters.generateClassChanges import world.respect.datalayer.db.school.adapters.toClassEntities import world.respect.datalayer.db.school.adapters.toEntities import world.respect.datalayer.db.school.adapters.toModel import world.respect.datalayer.exceptions.ForbiddenException +import world.respect.datalayer.school.ChangeHistoryLocal import world.respect.datalayer.school.ClassDataSource import world.respect.datalayer.school.ClassDataSourceLocal import world.respect.datalayer.school.model.Clazz import world.respect.datalayer.school.model.PermissionFlags import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.map -import kotlin.collections.map +import world.respect.libutil.util.time.systemTimeInMillis import kotlin.time.Clock class ClassDatasourceDb( @@ -30,7 +32,8 @@ class ClassDatasourceDb( private val uidNumberMapper: UidNumberMapper, @Suppress("unused") private val authenticatedUser: AuthenticatedUserPrincipalId, -) : ClassDataSourceLocal { + private val changeHistoryDataSource: ChangeHistoryLocal, + ) : ClassDataSourceLocal { private suspend fun doUpsertClass( @@ -119,7 +122,26 @@ class ClassDatasourceDb( if(!lastModAndPermissionInDb.hasPermission) throw ForbiddenException() + val timeNow = systemTimeInMillis() + + val oldClazz = schoolDb.getClassEntityDao().findByGuid( + guidHash = uidNumberMapper(clazz.guid) + ) + + if (oldClazz != null) { + val changeEntries = generateClassChanges( + hGuid = clazz.guid, + old = oldClazz.toClassEntities().toModel(), + new = clazz, + whoGuid = authenticatedUser.guid, + timestamp = timeNow, + hTableGuid = clazz.guid + ) + if (changeEntries != null) { + changeHistoryDataSource.store(listOf(changeEntries)) + } + } doUpsertClass(clazz) } } diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/PersonDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/PersonDataSourceDb.kt index 85f6921f6..8fea7416d 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/PersonDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/PersonDataSourceDb.kt @@ -22,12 +22,14 @@ import world.respect.datalayer.school.PersonDataSource import world.respect.datalayer.school.PersonDataSourceLocal import world.respect.datalayer.school.domain.CheckPersonPermissionUseCase import world.respect.datalayer.school.ext.primaryRole +import world.respect.datalayer.school.model.ChangeHistoryEntry import world.respect.datalayer.school.model.Person import world.respect.datalayer.school.model.composites.PersonListDetails import world.respect.datalayer.shared.maxLastModifiedOrNull import world.respect.datalayer.shared.maxLastStoredOrNull import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.map +import world.respect.lib.primarykeygen.PrimaryKeyGenerator import world.respect.libutil.util.time.atStartOfDayInMillisUtc import world.respect.libutil.util.time.systemTimeInMillis import kotlin.time.Clock @@ -38,6 +40,7 @@ class PersonDataSourceDb( private val authenticatedUser: AuthenticatedUserPrincipalId, private val checkPersonPermissionUseCase: CheckPersonPermissionUseCase, private val changeHistoryDataSource: ChangeHistoryLocal, + private val primaryKeyGenerator: PrimaryKeyGenerator ): PersonDataSourceLocal { @@ -98,7 +101,7 @@ class PersonDataSourceDb( ) if (oldPerson != null) { val changeEntries = generatePersonChanges( - hGuid = personToStore.guid, + primaryKeyGenerator = primaryKeyGenerator, old = oldPerson.toPersonEntities().toModel(), new = personToStore, whoGuid = authenticatedUser.guid, @@ -264,4 +267,11 @@ class PersonDataSourceDb( } } + override suspend fun storeWithHistory( + data: List, + changeHistory: List + ) { + TODO("Not yet implemented") + } + } \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt index 7375e7134..659794a16 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt @@ -8,8 +8,11 @@ import world.respect.datalayer.school.model.ChangeHistoryChange import world.respect.datalayer.school.model.ChangeHistoryEntry import world.respect.datalayer.school.model.ChangeHistoryFieldEnum import world.respect.datalayer.school.model.ChangeHistoryTableEnum +import world.respect.datalayer.school.model.Clazz +import world.respect.datalayer.school.model.Enrollment import world.respect.datalayer.school.model.Person import world.respect.datalayer.school.model.findDifference +import world.respect.lib.primarykeygen.PrimaryKeyGenerator data class ChangeHistoryEntities( @@ -32,9 +35,11 @@ fun ChangeHistoryWithChanges.toModel(): ChangeHistoryEntry { fun ChangeHistoryChangeEntity.toModel(): ChangeHistoryChange { return ChangeHistoryChange( + id = hcId, field = hcField, oldVal = hcOldVal, - newVal = hcNewVal + newVal = hcNewVal, + synced = hcSynced ) } @@ -56,10 +61,12 @@ fun ChangeHistoryEntry.toEntities( val changeEntities = changes.map { change -> ChangeHistoryChangeEntity( + hcId = change.id , hcHistoryGuidHash = guidHash, hcField = change.field, hcOldVal = change.oldVal, - hcNewVal = change.newVal + hcNewVal = change.newVal, + hcSynced = change.synced ) } @@ -68,8 +75,67 @@ fun ChangeHistoryEntry.toEntities( changeEntities = changeEntities ) } -fun generatePersonChanges( +fun generateClassChanges( + hGuid: String, + old: Clazz?, + new: Clazz, + whoGuid: String, + timestamp: Long, + hTableGuid: String +): ChangeHistoryEntry? { + + val changes = mutableListOf() + + + findDifference(ChangeHistoryFieldEnum.CLASS_TITLE, old?.title, new.title,changes) + findDifference(ChangeHistoryFieldEnum.CLASS_DESCRIPTION, old?.description, new.description,changes) + findDifference(ChangeHistoryFieldEnum.CLASS_STATUS, old?.status, new.status,changes) + + if (changes.isEmpty()) return null + + return ChangeHistoryEntry( + guid = hGuid, + table = ChangeHistoryTableEnum.CLASS, + tableGuid = hTableGuid, + whoGuid = whoGuid, + timestamp = timestamp, + changes = changes + ) +} + + +fun generateEnrollmentChanges( hGuid: String, + old: Enrollment?, + new: Enrollment, + whoGuid: String, + timestamp: Long, + hTableGuid: String +): ChangeHistoryEntry? { + + val changes = mutableListOf() + + + + findDifference(ChangeHistoryFieldEnum.ENROLLMENT_ROLE, old?.role, new.role,changes) + findDifference(ChangeHistoryFieldEnum.ENROLLMENT_BEGIN_DATE, old?.beginDate, new.beginDate,changes) + findDifference(ChangeHistoryFieldEnum.ENROLLMENT_END_DATE, old?.endDate, new.endDate,changes) + findDifference(ChangeHistoryFieldEnum.ENROLLMENT_STATUS, old?.status, new.status,changes) + + if (changes.isEmpty()) return null + + return ChangeHistoryEntry( + guid = hGuid, + table = ChangeHistoryTableEnum.ENROLLMENT, + tableGuid = hTableGuid, + whoGuid = whoGuid, + timestamp = timestamp, + changes = changes + ) +} + +fun generatePersonChanges( + primaryKeyGenerator: PrimaryKeyGenerator, old: Person?, new: Person, whoGuid: String, @@ -91,7 +157,7 @@ fun generatePersonChanges( if (changes.isEmpty()) return null return ChangeHistoryEntry( - guid = hGuid, + guid = primaryKeyGenerator.nextId(ChangeHistoryEntry.TABLE_ID).toString(), table = ChangeHistoryTableEnum.PERSON, timestamp = timestamp, whoGuid = whoGuid, diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt index ef4a2362e..a1d1c1a24 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt @@ -34,7 +34,32 @@ interface ChangeHistoryDao { insertHistory(history) insertChanges(changes) } + @Query(""" + SELECT * + FROM ChangeHistoryEntity + WHERE hTable = :tableEnum + AND hTableGuid IN (:uids) + """) + suspend fun getByTableAndUids( + tableEnum: ChangeHistoryTableEnum, + uids: List + ): List + @Query(""" + SELECT * + FROM ChangeHistoryEntity + WHERE hTable = :tableEnum + AND hTableGuid IN (:uids) + AND hGuidHash IN ( + SELECT hcHistoryGuidHash + FROM ChangeHistoryChangeEntity + WHERE hcSynced = 0 + ) + """) + suspend fun getParentsWithUnsyncedChanges( + tableEnum: ChangeHistoryTableEnum, + uids: List + ): List @Query(""" SELECT * FROM ChangeHistoryEntity @@ -46,7 +71,17 @@ interface ChangeHistoryDao { table: ChangeHistoryTableEnum?, whoGuidHash: Long, ): PagingSource + @Query(""" + SELECT * + FROM ChangeHistoryChangeEntity + WHERE hcHistoryGuidHash IN (:historyIds) + AND hcSynced = 0 + """) + suspend fun getUnsyncedChanges( + historyIds: List + ): List + @Transaction @Query( """ SELECT * @@ -56,14 +91,20 @@ interface ChangeHistoryDao { ) suspend fun findByGuid(tableGuid: String): List? - @Query(""" SELECT * FROM ChangeHistoryEntity - WHERE hGuid = :guid + WHERE hTableGuid = :guid """) - fun findByGuidAsFlow(guid: String): Flow + fun findByGuidAsFlow(guid: String): Flow> + + @Query(""" + UPDATE ChangeHistoryChangeEntity + SET hcSynced = 1 + WHERE hcHistoryGuidHash IN (:historyGuids) + """) + suspend fun markByHistoryGuids(historyGuids: List) @Query(""" SELECT * diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryChangeEntity.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryChangeEntity.kt index 9cc9ba7f5..2368686dc 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryChangeEntity.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryChangeEntity.kt @@ -3,6 +3,8 @@ package world.respect.datalayer.db.school.entities import androidx.room.Entity import androidx.room.PrimaryKey import world.respect.datalayer.school.model.ChangeHistoryFieldEnum +import kotlin.time.Clock +import kotlin.time.Instant @Entity data class ChangeHistoryChangeEntity( @@ -16,7 +18,11 @@ data class ChangeHistoryChangeEntity( val hcOldVal: String?, - val hcNewVal: String + val hcNewVal: String, + + val hcSynced: Boolean = false, + val hcLastModified: Instant = Clock.System.now(), + val hcStored: Instant = Clock.System.now(), ){ companion object{ const val CHILD_COLUMN = "hcHistoryGuidHash" diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryEntity.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryEntity.kt index 0ee0b6e28..8d74e93e2 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryEntity.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryEntity.kt @@ -3,6 +3,8 @@ package world.respect.datalayer.db.school.entities import androidx.room.Entity import androidx.room.PrimaryKey import world.respect.datalayer.school.model.ChangeHistoryTableEnum +import kotlin.time.Clock +import kotlin.time.Instant @Entity data class ChangeHistoryEntity( @@ -14,6 +16,8 @@ data class ChangeHistoryEntity( val hTimestamp: Long, val hWhoGuid: String, val hWhoGuidHash: Long, + val hLastModified: Instant = Clock.System.now(), + val hStored: Instant = Clock.System.now(), ){ companion object{ const val PARENT_COLUMN = "hGuidHash" diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/SchoolDataSourceHttp.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/SchoolDataSourceHttp.kt index ac26fa000..174affc96 100644 --- a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/SchoolDataSourceHttp.kt +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/SchoolDataSourceHttp.kt @@ -4,6 +4,8 @@ import io.ktor.client.HttpClient import io.ktor.http.Url import kotlinx.serialization.json.Json import world.respect.datalayer.AuthTokenProvider +import world.respect.datalayer.ChangeHistoryMarkSentToServer +import world.respect.datalayer.ChangeHistoryProvider import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.http.school.opds.OpdsPublicationDataSourceHttp import world.respect.datalayer.http.school.opds.OpdsFeedDataSourceHttp @@ -49,6 +51,8 @@ class SchoolDataSourceHttp( private val defaultAppCatalogUrl: String?, private val opdsFeedValidationHelper: BaseDataSourceValidationHelper? = null, private val opdsPublicationValidationHelper: BaseDataSourceValidationHelper? = null, + private val changeHistoryProvider: ChangeHistoryProvider, + private val markSentToServer: ChangeHistoryMarkSentToServer, ) : SchoolDataSource { override val schoolAppDataSource: SchoolAppDataSource by lazy { @@ -78,6 +82,8 @@ class SchoolDataSourceHttp( httpClient = httpClient, tokenProvider = tokenProvider, validationHelper = validationHelper, + changeHistoryProvider = changeHistoryProvider, + markSentToServer = markSentToServer, ) } diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ChangeHistoryDataSourceHttp.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ChangeHistoryDataSourceHttp.kt index 35748fba6..168e64e34 100644 --- a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ChangeHistoryDataSourceHttp.kt +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ChangeHistoryDataSourceHttp.kt @@ -1,20 +1,14 @@ package world.respect.datalayer.http.school import io.ktor.client.HttpClient -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import io.ktor.http.ContentType import io.ktor.http.URLBuilder import io.ktor.http.Url -import io.ktor.http.contentType import io.ktor.util.reflect.typeInfo import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map import world.respect.datalayer.AuthTokenProvider import world.respect.datalayer.DataLayerParams import world.respect.datalayer.DataLoadParams import world.respect.datalayer.DataLoadState -import world.respect.datalayer.ext.firstOrNotLoaded import world.respect.datalayer.ext.getAsDataLoadState import world.respect.datalayer.ext.getDataLoadResultAsFlow import world.respect.datalayer.ext.useTokenProvider @@ -52,6 +46,7 @@ class ChangeHistoryDataSourceHttp( loadParams: DataLoadParams, guid: String ): DataLoadState> { + return httpClient.getAsDataLoadState>( ChangeHistoryDataSource.GetListParams( common = GetListCommonParams(guid = guid) @@ -63,21 +58,31 @@ class ChangeHistoryDataSourceHttp( } override fun findByGuidAsFlow( - loadParams: DataLoadParams, guid: String - ): Flow> { + ): Flow>> { + return httpClient.getDataLoadResultAsFlow>( urlFn = { ChangeHistoryDataSource.GetListParams( common = GetListCommonParams(guid = guid) ).urlWithParams() }, - dataLoadParams = loadParams, + dataLoadParams = DataLoadParams() + ) { + useTokenProvider(tokenProvider) + useValidationCacheControl(validationHelper) + } + } + + override suspend fun list( + loadParams: DataLoadParams, + params: ChangeHistoryDataSource.GetListParams + ): DataLoadState> { + return httpClient.getAsDataLoadState( + url = params.urlWithParams() ) { useTokenProvider(tokenProvider) useValidationCacheControl(validationHelper) - }.map { - it.firstOrNotLoaded() } } @@ -101,13 +106,11 @@ class ChangeHistoryDataSourceHttp( } } + override suspend fun markSentToServer(changeHistoryEntries: List) { + TODO("Not yet implemented") + } + override suspend fun store(list: List) { - httpClient.post( - respectEndpointUrl(ChangeHistoryDataSource.ENDPOINT_NAME) - ) { - useTokenProvider(tokenProvider) - contentType(ContentType.Application.Json) - setBody(list) - } + } } diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/DataAndChangeHistory.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/DataAndChangeHistory.kt new file mode 100644 index 000000000..83a64de02 --- /dev/null +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/DataAndChangeHistory.kt @@ -0,0 +1,9 @@ +package world.respect.datalayer.http.school + +import kotlinx.serialization.Serializable +import world.respect.datalayer.school.model.ChangeHistoryEntry +@Serializable +data class DataAndChangeHistory( + val data: List, + val changeHistories: List +) \ No newline at end of file diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/PersonDataSourceHttp.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/PersonDataSourceHttp.kt index ce7c1d655..178735d6f 100644 --- a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/PersonDataSourceHttp.kt +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/PersonDataSourceHttp.kt @@ -11,6 +11,8 @@ import io.ktor.util.reflect.typeInfo import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import world.respect.datalayer.AuthTokenProvider +import world.respect.datalayer.ChangeHistoryMarkSentToServer +import world.respect.datalayer.ChangeHistoryProvider import world.respect.datalayer.DataLayerParams import world.respect.datalayer.DataLoadParams import world.respect.datalayer.DataLoadState @@ -26,7 +28,10 @@ import world.respect.datalayer.http.shared.paging.OffsetLimitHttpPagingSource import world.respect.datalayer.networkvalidation.ExtendedDataSourceValidationHelper import world.respect.datalayer.school.PersonDataSource import world.respect.datalayer.school.adapters.asListDetails +import world.respect.datalayer.school.model.ChangeHistoryEntry +import world.respect.datalayer.school.model.ChangeHistoryTableEnum import world.respect.datalayer.school.model.Person +import world.respect.datalayer.school.model.PersonWithHistoryRequest import world.respect.datalayer.school.model.composites.PersonListDetails import world.respect.datalayer.schooldirectory.SchoolDirectoryEntryDataSource import world.respect.datalayer.shared.paging.IPagingSourceFactory @@ -39,6 +44,8 @@ class PersonDataSourceHttp( private val httpClient: HttpClient, private val tokenProvider: AuthTokenProvider, private val validationHelper: ExtendedDataSourceValidationHelper?, + private val changeHistoryProvider: ChangeHistoryProvider, + private val markSentToServer: ChangeHistoryMarkSentToServer, ) : PersonDataSource, SchoolUrlBasedDataSource { private suspend fun PersonDataSource.GetListParams.urlWithParams(): Url { @@ -151,13 +158,48 @@ class PersonDataSourceHttp( } } + override suspend fun storeWithHistory( + data: List, + changeHistory: List + ) { + + httpClient.post( + respectEndpointUrl(PersonDataSource.ENDPOINT_NAME) + ) { + useTokenProvider(tokenProvider) + contentType(ContentType.Application.Json) + setBody( + PersonWithHistoryRequest( + person = data, + changeHistory = changeHistory + ) + ) + } + } + override suspend fun store(list: List) { + val changeHistories = changeHistoryProvider.getChangeHistoryEntries( + tableId = ChangeHistoryTableEnum.PERSON, + uids = list.map { it.guid } + ) httpClient.post( url = respectEndpointUrl(PersonDataSource.ENDPOINT_NAME) ) { useTokenProvider(tokenProvider) contentType(ContentType.Application.Json) - setBody(list) + + if (changeHistories.isEmpty()) { + setBody(list) + } else { + setBody>( + DataAndChangeHistory( + data = list, + changeHistories = changeHistories + ) + ) + } } + markSentToServer.markSentToServer(changeHistories) + } } \ No newline at end of file diff --git a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/ChangeHistoryDataSourceRepository.kt b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/ChangeHistoryDataSourceRepository.kt index f58d5a9ae..3737a9de8 100644 --- a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/ChangeHistoryDataSourceRepository.kt +++ b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/ChangeHistoryDataSourceRepository.kt @@ -6,7 +6,6 @@ import kotlinx.coroutines.flow.onEach import world.respect.datalayer.DataLoadParams import world.respect.datalayer.DataLoadState import world.respect.datalayer.ext.combineWithRemote -import world.respect.datalayer.ext.updateFromRemoteIfNeeded import world.respect.datalayer.ext.updateFromRemoteListIfNeeded import world.respect.datalayer.networkvalidation.ExtendedDataSourceValidationHelper import world.respect.datalayer.repository.shared.paging.RepositoryPagingSourceFactory @@ -31,6 +30,7 @@ class ChangeHistoryDataSourceRepository( loadParams: DataLoadParams, guid: String ): DataLoadState>{ + local.updateFromRemoteListIfNeeded( remoteLoad = remote.findByGuid(loadParams, guid), validationHelper = validationHelper, @@ -40,16 +40,23 @@ class ChangeHistoryDataSourceRepository( } override fun findByGuidAsFlow( - loadParams: DataLoadParams, guid: String - ): Flow> { - return local.findByGuidAsFlow(loadParams, guid).combineWithRemote( - remoteFlow = remote.findByGuidAsFlow(loadParams, guid).onEach { - local.updateFromRemoteIfNeeded(it, validationHelper) + ): Flow>> { + return local.findByGuidAsFlow(guid).combineWithRemote( + remoteFlow = remote.findByGuidAsFlow(guid).onEach { + local.updateFromRemoteListIfNeeded(it, validationHelper) } ) } - + override suspend fun list( + loadParams: DataLoadParams, + params: ChangeHistoryDataSource.GetListParams + ): DataLoadState> { + local.updateFromRemoteListIfNeeded( + remote.list(loadParams, params), validationHelper + ) + return local.list(loadParams, params) + } override fun listAsPagingSource( dataLoadParams: DataLoadParams, getListParams: ChangeHistoryDataSource.GetListParams @@ -71,6 +78,10 @@ class ChangeHistoryDataSourceRepository( ) } + override suspend fun markSentToServer(changeHistoryEntries: List) { + local.markSentToServer(changeHistoryEntries) + } + override suspend fun store(list: List) { local.store(list) diff --git a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/PersonDataSourceRepository.kt b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/PersonDataSourceRepository.kt index 2d125c57e..9e0b9b779 100644 --- a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/PersonDataSourceRepository.kt +++ b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/PersonDataSourceRepository.kt @@ -12,6 +12,7 @@ import world.respect.datalayer.repository.shared.paging.RepositoryPagingSourceFa import world.respect.datalayer.repository.shared.paging.loadAndUpdateLocal2 import world.respect.datalayer.school.PersonDataSource import world.respect.datalayer.school.PersonDataSourceLocal +import world.respect.datalayer.school.model.ChangeHistoryEntry import world.respect.datalayer.school.model.Person import world.respect.datalayer.school.model.composites.PersonListDetails import world.respect.datalayer.school.writequeue.RemoteWriteQueue @@ -111,6 +112,13 @@ class PersonDataSourceRepository( ) } + override suspend fun storeWithHistory( + data: List, + changeHistory: List + ) { + TODO("Not yet implemented") + } + override suspend fun store(list: List) { local.store(list) val timeNow = systemTimeInMillis() diff --git a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/writequeue/DrainRemoteWriteQueueUseCase.kt b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/writequeue/DrainRemoteWriteQueueUseCase.kt index 2aea24224..ecc5a9f5b 100644 --- a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/writequeue/DrainRemoteWriteQueueUseCase.kt +++ b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/writequeue/DrainRemoteWriteQueueUseCase.kt @@ -5,6 +5,7 @@ import io.ktor.http.Url import world.respect.datalayer.DataLoadParams import world.respect.datalayer.DataReadyState import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.ext.dataOrNull import world.respect.datalayer.repository.SchoolDataSourceRepository import world.respect.datalayer.school.writequeue.RemoteWriteQueue import world.respect.datalayer.school.writequeue.WriteQueueItem @@ -69,7 +70,6 @@ class DrainRemoteWriteQueueUseCase( repository.inviteDataSource.sendToRemote(listOf(item)) } WriteQueueItem.Model.CHANGE_HISTORY -> { - repository.changeHistoryDataSource.sendToRemote(listOf(item)) } diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/ChangeHistoryMarkSentToServer.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/ChangeHistoryMarkSentToServer.kt new file mode 100644 index 000000000..654b796ff --- /dev/null +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/ChangeHistoryMarkSentToServer.kt @@ -0,0 +1,9 @@ +package world.respect.datalayer + +import world.respect.datalayer.school.model.ChangeHistoryEntry + +interface ChangeHistoryMarkSentToServer { + suspend fun markSentToServer( + changeHistoryEntry: List + ) +} \ No newline at end of file diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/ChangeHistoryProvider.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/ChangeHistoryProvider.kt new file mode 100644 index 000000000..38e1e1853 --- /dev/null +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/ChangeHistoryProvider.kt @@ -0,0 +1,12 @@ +package world.respect.datalayer + +import world.respect.datalayer.school.model.ChangeHistoryEntry +import world.respect.datalayer.school.model.ChangeHistoryTableEnum + +interface ChangeHistoryProvider { + suspend fun getChangeHistoryEntries( + tableId: ChangeHistoryTableEnum, + uids: List + ): List +} + diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/ChangeHistoryDataSource.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/ChangeHistoryDataSource.kt index 372642b90..e157229ee 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/ChangeHistoryDataSource.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/ChangeHistoryDataSource.kt @@ -41,15 +41,23 @@ interface ChangeHistoryDataSource : WritableDataSource { ): DataLoadState> fun findByGuidAsFlow( - loadParams: DataLoadParams, guid: String - ): Flow> + ): Flow>> + + suspend fun list( + loadParams: DataLoadParams = DataLoadParams(), + params: GetListParams = GetListParams(), + ): DataLoadState> fun listAsPagingSource( dataLoadParams: DataLoadParams, getListParams: GetListParams ): IPagingSourceFactory + suspend fun markSentToServer( + changeHistoryEntries : List + ) + companion object { const val ENDPOINT_NAME = "changehistory" diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/PersonDataSource.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/PersonDataSource.kt index 2410bf85a..bc0bfa498 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/PersonDataSource.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/PersonDataSource.kt @@ -6,6 +6,7 @@ import kotlinx.datetime.LocalDate import world.respect.datalayer.DataLayerParams import world.respect.datalayer.DataLoadParams import world.respect.datalayer.DataLoadState +import world.respect.datalayer.school.model.ChangeHistoryEntry import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.Person import world.respect.datalayer.school.model.PersonRoleEnum @@ -93,7 +94,10 @@ interface PersonDataSource: WritableDataSource { loadParams: DataLoadParams, listParams: GetListParams, ): IPagingSourceFactory - + suspend fun storeWithHistory( + data: List, + changeHistory: List + ) /** * Persists the list to the DataSource. The underlying DataSource WILL set the stored time on diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/domain/CheckPersonPermissionUseCase.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/domain/CheckPersonPermissionUseCase.kt index 97241d78f..c0531f7e9 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/domain/CheckPersonPermissionUseCase.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/domain/CheckPersonPermissionUseCase.kt @@ -24,7 +24,12 @@ interface CheckPersonPermissionUseCase { ) companion object { - + val READ_PERMISSIONS = PermissionsRequiredByRole( + roleAdminPermissionRequired = PermissionFlags.SYSTEM_ADMIN, + roleTeacherPermissionRequired = PermissionFlags.PERSON_TEACHER_READ, + roleStudentPermissionRequired = PermissionFlags.PERSON_STUDENT_READ, + roleParentPermissionRequired = PermissionFlags.PERSON_PARENT_READ, + ) val WRITE_PERMISSIONS = PermissionsRequiredByRole( roleAdminPermissionRequired = PermissionFlags.SYSTEM_ADMIN, roleTeacherPermissionRequired = PermissionFlags.PERSON_TEACHER_WRITE, diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt index 97c3d020e..05ff408de 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt @@ -1,6 +1,10 @@ package world.respect.datalayer.school.model import kotlinx.serialization.Serializable +import world.respect.datalayer.shared.ModelWithTimes +import world.respect.lib.serializers.InstantAsISO8601 +import kotlin.time.Clock +import kotlin.time.Instant @Serializable data class ChangeHistoryEntry( @@ -9,8 +13,18 @@ data class ChangeHistoryEntry( val table: ChangeHistoryTableEnum, val timestamp: Long, val whoGuid: String, - val changes: List -) + val changes: List, + override val lastModified: InstantAsISO8601 = Clock.System.now(), + override val stored: InstantAsISO8601 = Clock.System.now(), +): ModelWithTimes { + + companion object { + + const val TABLE_ID = 31 + + } +} + @Serializable data class ChangeHistoryEntryWithWhoDid( @@ -20,7 +34,11 @@ data class ChangeHistoryEntryWithWhoDid( @Serializable data class ChangeHistoryChange( + val id : Long =0, val field: ChangeHistoryFieldEnum, val newVal: String, - val oldVal: String? -) \ No newline at end of file + val oldVal: String?, + val synced : Boolean = false, + override val lastModified: InstantAsISO8601 = Clock.System.now(), + override val stored: InstantAsISO8601 = Clock.System.now(), +): ModelWithTimes \ No newline at end of file diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/PersonWithHistoryRequest.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/PersonWithHistoryRequest.kt new file mode 100644 index 000000000..abd6fb944 --- /dev/null +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/PersonWithHistoryRequest.kt @@ -0,0 +1,9 @@ +package world.respect.datalayer.school.model + +import kotlinx.serialization.Serializable + +@Serializable +data class PersonWithHistoryRequest( + val person: List, + val changeHistory: List +) \ No newline at end of file diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/findDifference.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/findDifference.kt index 4fee2a81d..44b151c76 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/findDifference.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/findDifference.kt @@ -14,7 +14,7 @@ fun findDifference( ChangeHistoryChange( field = field, oldVal = oldString, - newVal = newString ?: "" + newVal = newString ?: "", ) ) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryViewModel.kt index a745b2be0..c5258fe2f 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryViewModel.kt @@ -4,7 +4,9 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.core.component.KoinScopeComponent @@ -22,7 +24,6 @@ import world.respect.shared.navigation.ChangeHistory import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState -import kotlin.getValue data class ChangeHistoryUiState( @@ -53,6 +54,44 @@ class ChangeHistoryViewModel( when (route.tableValue) { ChangeHistoryTableEnum.PERSON -> { + val changeHistoryFlow = + schoolDataSource.changeHistoryDataSource + .findByGuidAsFlow(route.guid) + .shareIn(viewModelScope, SharingStarted.Lazily, replay = 1) + + viewModelScope.launch { + + changeHistoryFlow.collect { state -> + + val changeHistoryList = state.dataOrNull() ?: emptyList() + + val grouped = changeHistoryList.groupBy { it.whoGuid } + + val resultList = grouped.mapNotNull { (whoGuid, entries) -> + + val person = schoolDataSource.personDataSource.findByGuid( + loadParams = DataLoadParams(), + guid = whoGuid + ).dataOrNull() + + person?.let { + ChangeHistoryEntryWithWhoDid( + person = it, + changeHistoryEntry = entries + ) + } + } + + _uiState.update { prev -> + prev.copy( + changeHistoryEntryWithWhoDid = resultList + ) + } + } + } + } + ChangeHistoryTableEnum.CLASS -> { + val changeHistoryList = schoolDataSource.changeHistoryDataSource .findByGuid( loadParams = DataLoadParams(), @@ -83,10 +122,6 @@ class ChangeHistoryViewModel( } } - ChangeHistoryTableEnum.CLASS -> { - - } - ChangeHistoryTableEnum.ENROLLMENT -> { } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index fb9550376..7328ed761 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -17,6 +17,7 @@ import world.respect.datalayer.DataLoadParams import world.respect.datalayer.DataLoadState import world.respect.datalayer.DataLoadingState import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.db.school.ext.isAdmin import world.respect.datalayer.ext.dataOrNull import world.respect.datalayer.school.EnrollmentDataSource import world.respect.datalayer.school.PersonDataSource @@ -52,9 +53,11 @@ import world.respect.shared.util.SortOrderOption import world.respect.shared.util.exception.getUiTextOrGeneric import world.respect.shared.util.ext.asUiText import world.respect.datalayer.db.school.ext.isAdminOrTeacher +import world.respect.datalayer.school.model.ChangeHistoryTableEnum import world.respect.datalayer.school.model.ClassInvite import world.respect.datalayer.school.model.ClassInviteModeEnum import world.respect.datalayer.school.writequeue.EnqueueRunPullSyncUseCase +import world.respect.shared.navigation.ChangeHistory import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState import world.respect.shared.viewmodel.app.appstate.Snack @@ -84,7 +87,8 @@ data class ClazzDetailUiState( val inviteCodePrefix: String? = null, val showAddStudent: Boolean = false, val showAddTeacher: Boolean = false, -) + val changeHistoryButtonVisible: Boolean = false, + ) class ClazzDetailViewModel( savedStateHandle: SavedStateHandle, @@ -183,6 +187,7 @@ class ClazzDetailViewModel( prev.copy( showAddStudent = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true, showAddTeacher = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true, + changeHistoryButtonVisible = selectedAccountAndPerson?.person?.isAdmin() == true, ) } @@ -291,7 +296,16 @@ class ClazzDetailViewModel( fun onToggleStudentsSection() { _uiState.update { it.copy(isStudentsExpanded = !it.isStudentsExpanded) } } - + fun onClickChangeHistoryButton(){ + _navCommandFlow.tryEmit( + NavCommand.Navigate( + ChangeHistory( + guid = route.guid, + table = ChangeHistoryTableEnum.CLASS.value + ) + ) + ) + } fun onClickEdit() { _navCommandFlow.tryEmit( NavCommand.Navigate(ClazzEdit(route.guid)) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/detail/PersonDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/detail/PersonDetailViewModel.kt index 708129493..e15cced9a 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/detail/PersonDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/detail/PersonDetailViewModel.kt @@ -106,7 +106,11 @@ class PersonDetailViewModel( otherPersonKnownRole = null, permissionsRequiredByRole = CheckPersonPermissionUseCase.PermissionsRequiredByRole.WRITE_PERMISSIONS, ) - + val hasReadPermission = checkPersonPermissionUseCase( + otherPersonUid = route.guid, + otherPersonKnownRole = null, + permissionsRequiredByRole = CheckPersonPermissionUseCase.PermissionsRequiredByRole.READ_PERMISSIONS, + ) _appUiState.update { prev -> prev.copy( title = personVal?.fullName()?.asUiText(), @@ -120,7 +124,7 @@ class PersonDetailViewModel( prev.copy( persons = persons, manageAccountVisible = hasAccountPermission && personVal?.username != null, - changeHistoryButtonVisible = activeAccount?.person?.isAdmin() == true, + changeHistoryButtonVisible = hasReadPermission, createAccountVisible = personVal != null && activeAccount?.person?.isAdminOrTeacher() == true && personVal.username == null, diff --git a/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/PersonRoute.kt b/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/PersonRoute.kt index 18440a078..faada201c 100644 --- a/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/PersonRoute.kt +++ b/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/PersonRoute.kt @@ -9,8 +9,14 @@ import io.ktor.server.response.respond import io.ktor.server.routing.Route import io.ktor.server.routing.get import io.ktor.server.routing.post +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromJsonElement import world.respect.datalayer.DataLoadParams import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.http.school.DataAndChangeHistory import world.respect.datalayer.school.PersonDataSource import world.respect.datalayer.school.model.Person import world.respect.server.util.ext.offsetLimitPagingLoadParams @@ -42,8 +48,31 @@ fun Route.PersonRoute( post(PersonDataSource.ENDPOINT_NAME) { val schoolDataSource = schoolDataSource(call) - val persons: List = call.receive() - schoolDataSource.personDataSource.store(persons) + + when (val incoming = call.receive()) { + + is JsonArray -> { + val persons = Json.decodeFromJsonElement>(incoming) + + schoolDataSource.personDataSource.store(persons) + } + + is JsonObject -> { + val request = Json.decodeFromJsonElement>(incoming) + + schoolDataSource.personDataSource.store(request.data) + + if (request.changeHistories.isNotEmpty()) { + schoolDataSource.changeHistoryDataSource.store(request.changeHistories) + schoolDataSource.changeHistoryDataSource.markSentToServer(request.changeHistories) + } + } + + else -> { + throw IllegalArgumentException("Invalid request format") + } + } + call.respond(HttpStatusCode.NoContent) } From 0b76e183d8e8dea026a800e162f186968e95ccb4 Mon Sep 17 00:00:00 2001 From: lenovo Date: Wed, 25 Mar 2026 15:39:22 +0530 Subject: [PATCH 13/21] commit --- .../datalayer/db/SchoolDataSourceDb.kt | 2 +- .../db/school/ChangeHistoryDataSourceDb.kt | 28 +++++++++++++++ .../datalayer/db/school/ClassDatasourceDb.kt | 4 ++- .../datalayer/db/school/PersonDataSourceDb.kt | 7 +--- .../school/adapters/ChangeHistoryAdapter.kt | 4 +-- .../db/school/daos/ChangeHistoryDao.kt | 8 +++++ .../datalayer/http/SchoolDataSourceHttp.kt | 2 ++ .../http/school/ClassDataSourceHttp.kt | 27 ++++++++++++-- .../http/school/PersonDataSourceHttp.kt | 17 --------- .../school/PersonDataSourceRepository.kt | 7 ---- .../datalayer/school/PersonDataSource.kt | 4 --- .../clazz/detail/ClazzDetailViewModel.kt | 10 +++++- .../routes/school/respect/ClassRoute.kt | 36 +++++++++++++++++-- 13 files changed, 111 insertions(+), 45 deletions(-) diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt index 79166d335..c8c437c12 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt @@ -111,7 +111,7 @@ class SchoolDataSourceDb( } override val classDataSource: ClassDataSourceLocal by lazy { - ClassDatasourceDb(schoolDb, uidNumberMapper, authenticatedUser,changeHistoryDataSource) + ClassDatasourceDb(schoolDb, uidNumberMapper, authenticatedUser,changeHistoryDataSource,primaryKeyGenerator) } override val inviteDataSource: InviteDataSourceLocal by lazy { diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt index 988226595..f3be820c0 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryDataSourceDb.kt @@ -10,6 +10,7 @@ import world.respect.datalayer.DataReadyState import world.respect.datalayer.NoDataLoadedState import world.respect.datalayer.UidNumberMapper import world.respect.datalayer.db.RespectSchoolDatabase +import world.respect.datalayer.db.school.adapters.ChangeHistoryEntities import world.respect.datalayer.db.school.adapters.toEntities import world.respect.datalayer.db.school.adapters.toModel import world.respect.datalayer.school.ChangeHistoryDataSource @@ -17,6 +18,7 @@ import world.respect.datalayer.school.ChangeHistoryLocal import world.respect.datalayer.school.model.ChangeHistoryEntry import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.map +import kotlin.time.Clock class ChangeHistoryDataSourceDb( private val schoolDb: RespectSchoolDatabase, @@ -111,6 +113,32 @@ class ChangeHistoryDataSourceDb( list: List, forceOverwrite: Boolean ) { + schoolDb.useWriterConnection { con -> + con.withTransaction(Transactor.SQLiteTransactionType.IMMEDIATE) { + val now = Clock.System.now() + + val toUpdate: List = list + .filter { + forceOverwrite || ( + schoolDb.getChangeHistoryDao().getLastModifiedByUidNum( + uidNum = uidNumberMapper(it.guid) + ) ?: 0L + ) < it.lastModified.toEpochMilliseconds() + } + .map { entry -> + entry.copy(stored = now).toEntities(uidNumberMapper) + } + + val dao = schoolDb.getChangeHistoryDao() + + toUpdate.forEach { item -> + dao.insertHistoryWithChanges( + history = item.changeHistoryEntity, + changes = item.changeEntities + ) + } + } + } } override suspend fun findByUidList(uids: List): List { diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ClassDatasourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ClassDatasourceDb.kt index f3beea151..0aed34d38 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ClassDatasourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ClassDatasourceDb.kt @@ -24,6 +24,7 @@ import world.respect.datalayer.school.model.Clazz import world.respect.datalayer.school.model.PermissionFlags import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.map +import world.respect.lib.primarykeygen.PrimaryKeyGenerator import world.respect.libutil.util.time.systemTimeInMillis import kotlin.time.Clock @@ -33,6 +34,7 @@ class ClassDatasourceDb( @Suppress("unused") private val authenticatedUser: AuthenticatedUserPrincipalId, private val changeHistoryDataSource: ChangeHistoryLocal, + private val primaryKeyGenerator: PrimaryKeyGenerator, ) : ClassDataSourceLocal { @@ -130,7 +132,7 @@ class ClassDatasourceDb( if (oldClazz != null) { val changeEntries = generateClassChanges( - hGuid = clazz.guid, + primaryKeyGenerator = primaryKeyGenerator, old = oldClazz.toClassEntities().toModel(), new = clazz, whoGuid = authenticatedUser.guid, diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/PersonDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/PersonDataSourceDb.kt index 8fea7416d..27d7b557c 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/PersonDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/PersonDataSourceDb.kt @@ -267,11 +267,6 @@ class PersonDataSourceDb( } } - override suspend fun storeWithHistory( - data: List, - changeHistory: List - ) { - TODO("Not yet implemented") - } + } \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt index 659794a16..a4125576e 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt @@ -76,7 +76,7 @@ fun ChangeHistoryEntry.toEntities( ) } fun generateClassChanges( - hGuid: String, + primaryKeyGenerator: PrimaryKeyGenerator, old: Clazz?, new: Clazz, whoGuid: String, @@ -94,7 +94,7 @@ fun generateClassChanges( if (changes.isEmpty()) return null return ChangeHistoryEntry( - guid = hGuid, + guid = primaryKeyGenerator.nextId(Clazz.TABLE_ID).toString(), table = ChangeHistoryTableEnum.CLASS, tableGuid = hTableGuid, whoGuid = whoGuid, diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt index a1d1c1a24..ac7ab3ff5 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt @@ -34,6 +34,14 @@ interface ChangeHistoryDao { insertHistory(history) insertChanges(changes) } + + @Query(""" + SELECT ChangeHistoryEntity.hLastModified + FROM ChangeHistoryEntity + WHERE ChangeHistoryEntity.hGuidHash = :uidNum + """) + suspend fun getLastModifiedByUidNum(uidNum: Long): Long? + @Query(""" SELECT * FROM ChangeHistoryEntity diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/SchoolDataSourceHttp.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/SchoolDataSourceHttp.kt index 174affc96..468e0ce8b 100644 --- a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/SchoolDataSourceHttp.kt +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/SchoolDataSourceHttp.kt @@ -120,6 +120,8 @@ class SchoolDataSourceHttp( httpClient = httpClient, tokenProvider = tokenProvider, validationHelper = validationHelper, + changeHistoryProvider = changeHistoryProvider, + markSentToServer = markSentToServer, ) } diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ClassDataSourceHttp.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ClassDataSourceHttp.kt index 5e725fcf0..fd9a4649d 100644 --- a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ClassDataSourceHttp.kt +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ClassDataSourceHttp.kt @@ -11,6 +11,8 @@ import io.ktor.util.reflect.typeInfo import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import world.respect.datalayer.AuthTokenProvider +import world.respect.datalayer.ChangeHistoryMarkSentToServer +import world.respect.datalayer.ChangeHistoryProvider import world.respect.datalayer.DataLoadParams import world.respect.datalayer.DataLoadState import world.respect.datalayer.ext.firstOrNotLoaded @@ -25,6 +27,7 @@ import world.respect.datalayer.http.shared.paging.OffsetLimitHttpPagingSource import world.respect.datalayer.networkvalidation.ExtendedDataSourceValidationHelper import world.respect.datalayer.school.ClassDataSource import world.respect.datalayer.school.ClassDataSource.Companion.PARAM_NAME_INVITE_CODE +import world.respect.datalayer.school.model.ChangeHistoryTableEnum import world.respect.datalayer.school.model.Clazz import world.respect.datalayer.schooldirectory.SchoolDirectoryEntryDataSource import world.respect.datalayer.shared.paging.IPagingSourceFactory @@ -36,7 +39,9 @@ class ClassDataSourceHttp( private val httpClient: HttpClient, private val tokenProvider: AuthTokenProvider, private val validationHelper: ExtendedDataSourceValidationHelper?, -) : ClassDataSource, SchoolUrlBasedDataSource { + private val changeHistoryProvider: ChangeHistoryProvider, + private val markSentToServer: ChangeHistoryMarkSentToServer + ) : ClassDataSource, SchoolUrlBasedDataSource { private suspend fun ClassDataSource.GetListParams.urlWithParams(): Url { return URLBuilder(respectEndpointUrl(ClassDataSource.ENDPOINT_NAME)) @@ -108,12 +113,28 @@ class ClassDataSourceHttp( } override suspend fun store(list: List) { + val changeHistories = changeHistoryProvider.getChangeHistoryEntries( + tableId = ChangeHistoryTableEnum.CLASS, + uids = list.map { it.guid } + ) httpClient.post( - respectEndpointUrl(ClassDataSource.ENDPOINT_NAME) + url = respectEndpointUrl(ClassDataSource.ENDPOINT_NAME) ) { useTokenProvider(tokenProvider) contentType(ContentType.Application.Json) - setBody(list) + + if (changeHistories.isEmpty()) { + setBody(list) + } else { + setBody>( + DataAndChangeHistory( + data = list, + changeHistories = changeHistories + ) + ) + } } + markSentToServer.markSentToServer(changeHistories) + } } \ No newline at end of file diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/PersonDataSourceHttp.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/PersonDataSourceHttp.kt index 178735d6f..e80b4ef0b 100644 --- a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/PersonDataSourceHttp.kt +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/PersonDataSourceHttp.kt @@ -158,24 +158,7 @@ class PersonDataSourceHttp( } } - override suspend fun storeWithHistory( - data: List, - changeHistory: List - ) { - httpClient.post( - respectEndpointUrl(PersonDataSource.ENDPOINT_NAME) - ) { - useTokenProvider(tokenProvider) - contentType(ContentType.Application.Json) - setBody( - PersonWithHistoryRequest( - person = data, - changeHistory = changeHistory - ) - ) - } - } override suspend fun store(list: List) { val changeHistories = changeHistoryProvider.getChangeHistoryEntries( diff --git a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/PersonDataSourceRepository.kt b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/PersonDataSourceRepository.kt index 9e0b9b779..5d0344481 100644 --- a/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/PersonDataSourceRepository.kt +++ b/respect-datalayer-repository/src/commonMain/kotlin/world/respect/datalayer/repository/school/PersonDataSourceRepository.kt @@ -112,13 +112,6 @@ class PersonDataSourceRepository( ) } - override suspend fun storeWithHistory( - data: List, - changeHistory: List - ) { - TODO("Not yet implemented") - } - override suspend fun store(list: List) { local.store(list) val timeNow = systemTimeInMillis() diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/PersonDataSource.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/PersonDataSource.kt index bc0bfa498..8822fdd92 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/PersonDataSource.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/PersonDataSource.kt @@ -94,10 +94,6 @@ interface PersonDataSource: WritableDataSource { loadParams: DataLoadParams, listParams: GetListParams, ): IPagingSourceFactory - suspend fun storeWithHistory( - data: List, - changeHistory: List - ) /** * Persists the list to the DataSource. The underlying DataSource WILL set the stored time on diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 7328ed761..64b1c7a79 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -53,6 +53,7 @@ import world.respect.shared.util.SortOrderOption import world.respect.shared.util.exception.getUiTextOrGeneric import world.respect.shared.util.ext.asUiText import world.respect.datalayer.db.school.ext.isAdminOrTeacher +import world.respect.datalayer.school.domain.CheckPersonPermissionUseCase import world.respect.datalayer.school.model.ChangeHistoryTableEnum import world.respect.datalayer.school.model.ClassInvite import world.respect.datalayer.school.model.ClassInviteModeEnum @@ -111,6 +112,8 @@ class ClazzDetailViewModel( private val route: ClazzDetail = savedStateHandle.toRoute() + private val checkPersonPermissionUseCase: CheckPersonPermissionUseCase by inject() + private fun pagingSourceByRole(role: EnrollmentRoleEnum): PagingSourceFactoryHolder { return PagingSourceFactoryHolder { schoolDataSource.personDataSource.listAsPagingSource( @@ -183,11 +186,16 @@ class ClazzDetailViewModel( viewModelScope.launch { _uiState.whenSubscribed { accountManager.selectedAccountAndPersonFlow.collect { selectedAccountAndPerson -> + val hasReadPermission = checkPersonPermissionUseCase( + otherPersonUid = selectedAccountAndPerson?.person?.guid?:"", + otherPersonKnownRole = null, + permissionsRequiredByRole = CheckPersonPermissionUseCase.PermissionsRequiredByRole.READ_PERMISSIONS, + ) _uiState.update { prev -> prev.copy( showAddStudent = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true, showAddTeacher = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true, - changeHistoryButtonVisible = selectedAccountAndPerson?.person?.isAdmin() == true, + changeHistoryButtonVisible = hasReadPermission, ) } diff --git a/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/ClassRoute.kt b/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/ClassRoute.kt index 1c0f353aa..1c94dccd5 100644 --- a/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/ClassRoute.kt +++ b/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/ClassRoute.kt @@ -9,9 +9,16 @@ import io.ktor.server.response.respond import io.ktor.server.routing.Route import io.ktor.server.routing.get import io.ktor.server.routing.post +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromJsonElement import world.respect.datalayer.DataLoadParams import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.http.school.DataAndChangeHistory import world.respect.datalayer.school.ClassDataSource +import world.respect.datalayer.school.model.Clazz import world.respect.server.util.ext.offsetLimitPagingLoadParams import world.respect.server.util.ext.requireAccountScope import world.respect.server.util.ext.respondOffsetLimitPaging @@ -36,9 +43,32 @@ fun Route.ClassRoute( } post(ClassDataSource.ENDPOINT_NAME) { - schoolDataSource(call).classDataSource.store( - list = call.receive() - ) + val schoolDataSource = schoolDataSource(call) + + when (val incoming = call.receive()) { + + is JsonArray -> { + val clazz = Json.decodeFromJsonElement>(incoming) + + schoolDataSource.classDataSource.store(clazz) + } + + is JsonObject -> { + val request = Json.decodeFromJsonElement>(incoming) + + schoolDataSource.classDataSource.store(request.data) + + if (request.changeHistories.isNotEmpty()) { + schoolDataSource.changeHistoryDataSource.store(request.changeHistories) + schoolDataSource.changeHistoryDataSource.markSentToServer(request.changeHistories) + } + } + + else -> { + throw IllegalArgumentException("Invalid request format") + } + } + call.respond(HttpStatusCode.NoContent) } From 5636c41202ac5c253fe6ded5236ddd91b43d5520 Mon Sep 17 00:00:00 2001 From: lenovo Date: Wed, 25 Mar 2026 18:59:20 +0530 Subject: [PATCH 14/21] commit --- .../001_001_invite_users_using_qr_code_or_link_test.yaml | 4 ++-- .../test/clientservertest/ClientServerDatasourceTest.kt | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml index ea5750c67..0a573f3c4 100644 --- a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml +++ b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml @@ -104,8 +104,8 @@ onFlowComplete: - tapOn: "People" - tapOn: - id: "floating_action_bu- tapOn: "TeacherA User"tton" # Edit button -- tapOn: "First names*" + id: "floating_action_button" +- tapOn: "TeacherA User" - runFlow: file: "subflows/erase_text.yaml" env: diff --git a/respect-lib-test/src/jvmMain/kotlin/world/respect/lib/test/clientservertest/ClientServerDatasourceTest.kt b/respect-lib-test/src/jvmMain/kotlin/world/respect/lib/test/clientservertest/ClientServerDatasourceTest.kt index 628e2eebb..7decb6125 100644 --- a/respect-lib-test/src/jvmMain/kotlin/world/respect/lib/test/clientservertest/ClientServerDatasourceTest.kt +++ b/respect-lib-test/src/jvmMain/kotlin/world/respect/lib/test/clientservertest/ClientServerDatasourceTest.kt @@ -31,6 +31,8 @@ import world.respect.datalayer.db.RespectAppDatabase import world.respect.datalayer.db.RespectSchoolDatabase import world.respect.datalayer.db.SchoolDataSourceDb import world.respect.datalayer.db.networkvalidation.ExtendedDataSourceValidationHelperImpl +import world.respect.datalayer.db.school.ChangeHistoryMarkSentToServerDb +import world.respect.datalayer.db.school.ChangeHistoryProviderDb import world.respect.datalayer.db.school.domain.AddDefaultSchoolPermissionGrantsUseCase import world.respect.datalayer.db.school.domain.CheckPersonPermissionUseCaseDbImpl import world.respect.datalayer.db.school.writequeue.RemoteWriteQueueDbImpl @@ -235,14 +237,18 @@ class ClientServerDataSourceTestBuilder internal constructor( ) val token = "secret" + val changeHistoryProvider = ChangeHistoryProviderDb(schoolDb) + val markSentToServer = ChangeHistoryMarkSentToServerDb(schoolDb, uidNumberMapper =XXHashUidNumberMapper(stringHasher) ) val schoolDataSourceRemote = SchoolDataSourceHttp( schoolUrl = schoolUrl, schoolDirectoryEntryDataSource = clientAppDataSource.schoolDirectoryEntryDataSource, httpClient = httpClient, - tokenProvider = { AuthToken(token, systemTimeInMillis(), 3600) }, + tokenProvider = { AuthToken(token, systemTimeInMillis(), 3600) }, validationHelper = clientValidationHelper, defaultAppCatalogUrl = null, json = json, + changeHistoryProvider = changeHistoryProvider, + markSentToServer = markSentToServer, ) val drainQueueSignal = Channel(capacity = Channel.UNLIMITED) From 32d855ef6f143f2b6881dd33ad9007a83a1e944b Mon Sep 17 00:00:00 2001 From: lenovo Date: Wed, 25 Mar 2026 23:58:18 +0530 Subject: [PATCH 15/21] commit --- ...vite_users_using_qr_code_or_link_test.yaml | 14 +-- .../apps/changehistory/ChangeHistoryScreen.kt | 35 +++++-- .../datalayer/db/SchoolDataSourceDb.kt | 2 +- .../db/school/EnrollmentDataSourceDb.kt | 62 ++++++++++--- .../school/adapters/ChangeHistoryAdapter.kt | 92 ++++++++++++++++--- .../school/model/ChangeHistoryTableEnum.kt | 17 ++-- 6 files changed, 175 insertions(+), 47 deletions(-) diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml index 0a573f3c4..e35ca0855 100644 --- a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml +++ b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml @@ -102,15 +102,16 @@ onFlowComplete: - assertVisible: "Apps" - assertVisible: "Assignments" - tapOn: "People" - +- tapOn: "TeacherA User" - tapOn: id: "floating_action_button" -- tapOn: "TeacherA User" +- tapOn: "First names*" - runFlow: file: "subflows/erase_text.yaml" env: TEXT: "First names*" - inputText: "TeacherB" +- tapOn: "Date of birth" - runScript: file: "scripts/setDate.js" - inputText: ${output.pastYearDateC} @@ -119,8 +120,7 @@ onFlowComplete: - assertVisible: id: "app_title" text: "Change History" -- assertVisible: "Person name changed from “TeacherA User” to “TeacherB User”" - +- assertVisible: 'Person name changed from "TeacherA User" to "TeacherB User"' - runFlow: file: "subflows/add_class.yaml" env: @@ -223,14 +223,14 @@ onFlowComplete: - assertVisible: id: "app_title" text: "Change History" -- assertVisible: "Join request approved for “Student User”" -- assertVisible: "Teacher added: “TeacherB User”" +- assertVisible: 'Join request approved for “Student User”' +- tapOn: "People" - tapOn: "TeacherB User" - tapOn: "Change History" - assertVisible: id: "app_title" text: "Change History" -- assertVisible: "Person name changed from “TeacherA User” to “TeacherB User”" +- assertVisible: 'Person name changed from “TeacherA User” to “TeacherB User”' # E) Teacher share invite link for parent device based signup - tapOn: "Add student" diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt index 7c95e5ed1..da211e81b 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.unit.dp import kotlinx.datetime.TimeZone import org.jetbrains.compose.resources.stringResource import world.respect.datalayer.db.school.ext.fullName +import world.respect.datalayer.school.model.ChangeHistoryFieldEnum import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.change_format import world.respect.shared.util.rememberFormattedDateTime @@ -62,14 +63,32 @@ fun ChangeHistoryScreen( ) Text(text = createdAtStr) - Text( - text = stringResource( - Res.string.change_format, - change.field.displayName, - change.oldVal?:"", - change.newVal - ) - ) + val text = when (change.field) { + + ChangeHistoryFieldEnum.JOIN_REQUEST_APPROVED -> + "${change.field.displayName} \"${change.newVal}\"" + + ChangeHistoryFieldEnum.JOIN_REQUEST_REJECTED -> + "${change.field.displayName} \"${change.oldVal}\"" + + ChangeHistoryFieldEnum.CLASS_TEACHER_ADDED, + ChangeHistoryFieldEnum.CLASS_STUDENT_ADDED -> + "${change.field.displayName}: \"${change.newVal}\"" + + ChangeHistoryFieldEnum.CLASS_TEACHER_REMOVED, + ChangeHistoryFieldEnum.CLASS_STUDENT_REMOVED -> + "${change.field.displayName}: \"${change.oldVal}\"" + + else -> + stringResource( + Res.string.change_format, + change.field.displayName, + change.oldVal ?: "", + change.newVal + ) + } + + Text(text = text) Spacer(modifier = Modifier.height(16.dp)) } diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt index c8c437c12..cf9de3231 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/SchoolDataSourceDb.kt @@ -119,7 +119,7 @@ class SchoolDataSourceDb( } override val enrollmentDataSource: EnrollmentDataSourceLocal by lazy { - EnrollmentDataSourceDb(schoolDb, uidNumberMapper, authenticatedUser) + EnrollmentDataSourceDb(schoolDb, uidNumberMapper, authenticatedUser,changeHistoryDataSource,primaryKeyGenerator) } override val assignmentDataSource: AssignmentDataSourceLocal by lazy { diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/EnrollmentDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/EnrollmentDataSourceDb.kt index 0f8a4af86..b082aaec9 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/EnrollmentDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/EnrollmentDataSourceDb.kt @@ -12,8 +12,12 @@ import world.respect.datalayer.DataReadyState import world.respect.datalayer.NoDataLoadedState import world.respect.datalayer.UidNumberMapper import world.respect.datalayer.db.RespectSchoolDatabase +import world.respect.datalayer.db.school.adapters.generateEnrollmentChanges import world.respect.datalayer.db.school.adapters.toEntities import world.respect.datalayer.db.school.adapters.toModel +import world.respect.datalayer.db.school.adapters.toPersonEntities +import world.respect.datalayer.db.school.ext.fullName +import world.respect.datalayer.school.ChangeHistoryLocal import world.respect.datalayer.school.EnrollmentDataSource import world.respect.datalayer.school.EnrollmentDataSourceLocal import world.respect.datalayer.school.model.Enrollment @@ -23,6 +27,7 @@ import world.respect.datalayer.shared.maxLastModifiedOrNull import world.respect.datalayer.shared.maxLastStoredOrNull import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.map +import world.respect.lib.primarykeygen.PrimaryKeyGenerator import world.respect.libutil.util.throwable.ForbiddenException import world.respect.libutil.util.time.atStartOfDayInMillisUtc import kotlin.collections.map @@ -34,6 +39,8 @@ class EnrollmentDataSourceDb( private val uidNumberMapper: UidNumberMapper, @Suppress("unused") private val authenticatedUser: AuthenticatedUserPrincipalId, + private val changeHistoryDataSource: ChangeHistoryLocal, + private val primaryKeyGenerator: PrimaryKeyGenerator, ) : EnrollmentDataSourceLocal { private val logPrefix: String by lazy { @@ -126,27 +133,55 @@ class EnrollmentDataSourceDb( } override suspend fun store(list: List) { - if(list.isEmpty()) - return + if (list.isEmpty()) return schoolDb.useWriterConnection { con -> con.withTransaction(Transactor.SQLiteTransactionType.IMMEDIATE) { val now = Clock.System.now() + list.forEach { enrollment -> - val classPermissionResult = schoolDb.getClassEntityDao().getLastModifiedAndHasPermission( - authenticatedPersonUidNum = uidNumberMapper(authenticatedUser.guid), - classUidNum = uidNumberMapper(enrollment.classUid), - requiredPermission = when(enrollment.role) { - EnrollmentRoleEnum.STUDENT, EnrollmentRoleEnum.PENDING_STUDENT -> { - PermissionFlags.CLASS_WRITE_STUDENT_ENROLLMENT + + val classPermissionResult = schoolDb.getClassEntityDao() + .getLastModifiedAndHasPermission( + authenticatedPersonUidNum = uidNumberMapper(authenticatedUser.guid), + classUidNum = uidNumberMapper(enrollment.classUid), + requiredPermission = when (enrollment.role) { + EnrollmentRoleEnum.STUDENT, + EnrollmentRoleEnum.PENDING_STUDENT -> + PermissionFlags.CLASS_WRITE_STUDENT_ENROLLMENT + else -> + PermissionFlags.CLASS_WRITE_TEACHER_ENROLLMENT } - else -> PermissionFlags.CLASS_WRITE_TEACHER_ENROLLMENT - } + ) + + if (!enrollment.role.requiresApproval && !classPermissionResult.hasPermission) { + throw ForbiddenException( + "$logPrefix no permission to enrol in class ${enrollment.classUid}" + ) + } + + val existing = schoolDb.getEnrollmentEntityDao().findByGuid( + uidNum = uidNumberMapper(enrollment.uid) + )?.toModel() + + val personName = schoolDb.getPersonEntityDao() + .findByGuidNum(uidNumberMapper(enrollment.personUid)) + ?.toPersonEntities() + ?.toModel()?.fullName() + + val changeEntry = generateEnrollmentChanges( + hGuid = primaryKeyGenerator.nextId(Enrollment.TABLE_ID).toString(), + old = existing, + new = enrollment, + whoGuid = authenticatedUser.guid, + timestamp = now.toEpochMilliseconds(), + hTableGuid = enrollment.classUid, + personName = personName ) - if(!enrollment.role.requiresApproval && !classPermissionResult.hasPermission) - throw ForbiddenException("$logPrefix no permission to enrol in " + - "class ${enrollment.classUid}") + if (changeEntry != null) { + changeHistoryDataSource.store(listOf(changeEntry)) + } schoolDb.getEnrollmentEntityDao().upsert( listOf( @@ -159,7 +194,6 @@ class EnrollmentDataSourceDb( } } } - override suspend fun updateLocal( list: List, forceOverwrite: Boolean diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt index a4125576e..f3b4faca1 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt @@ -4,13 +4,16 @@ import world.respect.datalayer.UidNumberMapper import world.respect.datalayer.db.school.entities.ChangeHistoryChangeEntity import world.respect.datalayer.db.school.entities.ChangeHistoryEntity import world.respect.datalayer.db.school.entities.ChangeHistoryWithChanges +import world.respect.datalayer.db.school.ext.fullName import world.respect.datalayer.school.model.ChangeHistoryChange import world.respect.datalayer.school.model.ChangeHistoryEntry import world.respect.datalayer.school.model.ChangeHistoryFieldEnum import world.respect.datalayer.school.model.ChangeHistoryTableEnum import world.respect.datalayer.school.model.Clazz import world.respect.datalayer.school.model.Enrollment +import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.Person +import world.respect.datalayer.school.model.StatusEnum import world.respect.datalayer.school.model.findDifference import world.respect.lib.primarykeygen.PrimaryKeyGenerator @@ -91,6 +94,8 @@ fun generateClassChanges( findDifference(ChangeHistoryFieldEnum.CLASS_DESCRIPTION, old?.description, new.description,changes) findDifference(ChangeHistoryFieldEnum.CLASS_STATUS, old?.status, new.status,changes) + findDifference(ChangeHistoryFieldEnum.CLASS_STATUS, old?.status, new.status, changes) + if (changes.isEmpty()) return null return ChangeHistoryEntry( @@ -104,23 +109,90 @@ fun generateClassChanges( } +private fun roleChangeField( + oldRole: EnrollmentRoleEnum?, + newRole: EnrollmentRoleEnum +): ChangeHistoryFieldEnum? { + return when { + oldRole == EnrollmentRoleEnum.PENDING_STUDENT && newRole == EnrollmentRoleEnum.STUDENT -> + ChangeHistoryFieldEnum.JOIN_REQUEST_APPROVED + + oldRole == EnrollmentRoleEnum.PENDING_TEACHER && newRole == EnrollmentRoleEnum.TEACHER -> + ChangeHistoryFieldEnum.JOIN_REQUEST_APPROVED + + else -> null + } +} + + fun generateEnrollmentChanges( hGuid: String, old: Enrollment?, new: Enrollment, whoGuid: String, timestamp: Long, - hTableGuid: String + hTableGuid: String, + personName: String? ): ChangeHistoryEntry? { val changes = mutableListOf() - - - - findDifference(ChangeHistoryFieldEnum.ENROLLMENT_ROLE, old?.role, new.role,changes) - findDifference(ChangeHistoryFieldEnum.ENROLLMENT_BEGIN_DATE, old?.beginDate, new.beginDate,changes) - findDifference(ChangeHistoryFieldEnum.ENROLLMENT_END_DATE, old?.endDate, new.endDate,changes) - findDifference(ChangeHistoryFieldEnum.ENROLLMENT_STATUS, old?.status, new.status,changes) + val name = personName ?: new.personUid + + if (old == null) { + changes += ChangeHistoryChange( + field = when (new.role) { + EnrollmentRoleEnum.TEACHER -> ChangeHistoryFieldEnum.CLASS_TEACHER_ADDED + EnrollmentRoleEnum.STUDENT -> ChangeHistoryFieldEnum.CLASS_STUDENT_ADDED + else -> ChangeHistoryFieldEnum.CLASS_STUDENT_ADDED + }, + oldVal = null, + newVal = name + ) + } else { + + if (new.removedAt != null && old.removedAt == null) { + changes += ChangeHistoryChange( + field = when (new.role) { + EnrollmentRoleEnum.TEACHER -> ChangeHistoryFieldEnum.CLASS_TEACHER_REMOVED + EnrollmentRoleEnum.STUDENT -> ChangeHistoryFieldEnum.CLASS_STUDENT_REMOVED + else -> ChangeHistoryFieldEnum.CLASS_STUDENT_REMOVED + }, + oldVal = name, + newVal = "" + ) + + return ChangeHistoryEntry( + guid = hGuid, + table = ChangeHistoryTableEnum.CLASS, + tableGuid = hTableGuid, + whoGuid = whoGuid, + timestamp = timestamp, + changes = changes + ) + } + + roleChangeField(old.role, new.role)?.let { + changes += ChangeHistoryChange( + field = it, + oldVal = name, + newVal = name + ) + + return ChangeHistoryEntry( + guid = hGuid, + table = ChangeHistoryTableEnum.CLASS, + tableGuid = hTableGuid, + whoGuid = whoGuid, + timestamp = timestamp, + changes = changes + ) + } + + findDifference(ChangeHistoryFieldEnum.ENROLLMENT_ROLE, old.role, new.role, changes) + findDifference(ChangeHistoryFieldEnum.ENROLLMENT_BEGIN_DATE, old.beginDate, new.beginDate, changes) + findDifference(ChangeHistoryFieldEnum.ENROLLMENT_END_DATE, old.endDate, new.endDate, changes) + findDifference(ChangeHistoryFieldEnum.ENROLLMENT_STATUS, old.status, new.status, changes) + } if (changes.isEmpty()) return null @@ -145,9 +217,7 @@ fun generatePersonChanges( val changes = mutableListOf() - findDifference(ChangeHistoryFieldEnum.PERSON_GIVEN_NAME, old?.givenName, new.givenName, changes) - findDifference(ChangeHistoryFieldEnum.PERSON_FAMILY_NAME, old?.familyName, new.familyName, changes) - findDifference(ChangeHistoryFieldEnum.PERSON_MIDDLE_NAME, old?.middleName, new.middleName, changes) + findDifference(ChangeHistoryFieldEnum.PERSON_GIVEN_NAME, old?.fullName(), new.fullName(), changes) findDifference(ChangeHistoryFieldEnum.PERSON_USERNAME, old?.username, new.username, changes) findDifference(ChangeHistoryFieldEnum.PERSON_GENDER, old?.gender, new.gender, changes) findDifference(ChangeHistoryFieldEnum.PERSON_EMAIL, old?.email, new.email, changes) diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryTableEnum.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryTableEnum.kt index 1ae765e9b..e4b3a5541 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryTableEnum.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryTableEnum.kt @@ -17,19 +17,24 @@ enum class ChangeHistoryFieldEnum( val displayName: String ) { - PERSON_GIVEN_NAME("pGivenName", "First Name"), - PERSON_FAMILY_NAME("pFamilyName", "Last Name"), - PERSON_MIDDLE_NAME("pMiddleName", "Middle Name"), + PERSON_GIVEN_NAME("pGivenName", "Person name"), + PERSON_FAMILY_NAME("pFamilyName", "Person name"), + PERSON_MIDDLE_NAME("pMiddleName", "Person name"), PERSON_USERNAME("pUsername", "Username"), PERSON_GENDER("pGender", "Gender"), PERSON_EMAIL("pEmail", "Email"), PERSON_PHONE_NUMBER("pPhoneNumber", "Phone Number"), - PERSON_DATE_OF_BIRTH("pDateOfBirth", "Date of Birth"), + PERSON_DATE_OF_BIRTH("pDateOfBirth", "date of birth"), + CLASS_STATUS("cStatus", "Class Status"), CLASS_TITLE("cTitle", "Class Title"), CLASS_DESCRIPTION("cDescription", "Class Description"), - CLASS_STATUS("cStatus", "Class Status"), - + CLASS_TEACHER_ADDED("cTeacherAdded", "Teacher Added"), + CLASS_TEACHER_REMOVED("cTeacherRemoved", "Teacher Removed"), + CLASS_STUDENT_ADDED("cStudentAdded", "Student Added"), + CLASS_STUDENT_REMOVED("cStudentRemoved", "Student Removed"), + JOIN_REQUEST_APPROVED("joinRequestApproved", "Join request approved for"), + JOIN_REQUEST_REJECTED("joinRequestRejected", "Join request rejected for"), ENROLLMENT_ROLE("eRole", "Role"), ENROLLMENT_BEGIN_DATE("eBeginDate", "Start Date"), ENROLLMENT_END_DATE("eEndDate", "End Date"), From e422c2d5a166935165cae22aa4b6a626e987c63a Mon Sep 17 00:00:00 2001 From: lenovo Date: Thu, 26 Mar 2026 00:38:31 +0530 Subject: [PATCH 16/21] commit --- .../001_001_invite_users_using_qr_code_or_link_test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml index e35ca0855..7dba8c6b8 100644 --- a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml +++ b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml @@ -223,14 +223,14 @@ onFlowComplete: - assertVisible: id: "app_title" text: "Change History" -- assertVisible: 'Join request approved for “Student User”' +- assertVisible: 'Join request approved for "Student User"' - tapOn: "People" - tapOn: "TeacherB User" - tapOn: "Change History" - assertVisible: id: "app_title" text: "Change History" -- assertVisible: 'Person name changed from “TeacherA User” to “TeacherB User”' +- assertVisible: 'Person name changed from "TeacherA User" to "TeacherB User"' # E) Teacher share invite link for parent device based signup - tapOn: "Add student" From f3cf255fe1a9f8451ed2931f6f124caf6206b03b Mon Sep 17 00:00:00 2001 From: lenovo Date: Thu, 26 Mar 2026 12:31:27 +0530 Subject: [PATCH 17/21] commit --- .../apps/changehistory/ChangeHistoryScreen.kt | 4 +-- .../14.json | 12 ++----- .../db/RespectSchoolDatabaseMigrations.kt | 1 - .../school/adapters/ChangeHistoryAdapter.kt | 22 ++++-------- .../db/school/daos/ChangeHistoryDao.kt | 3 +- .../db/school/entities/ChangeHistoryEntity.kt | 1 - .../datalayer/http/SchoolDataSourceHttp.kt | 2 ++ .../http/school/EnrollmentDataSourceHttp.kt | 34 +++++++++++++----- .../school/model/ChangeHistoryEntry.kt | 1 - .../routes/school/respect/EnrollmentRoute.kt | 36 +++++++++++++++++-- 10 files changed, 73 insertions(+), 43 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt index da211e81b..7eeae15ad 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt @@ -58,7 +58,7 @@ fun ChangeHistoryScreen( text = group.person.fullName(), ) val createdAtStr = rememberFormattedDateTime( - timeInMillis = entry.timestamp, + timeInMillis = entry.lastModified.toEpochMilliseconds(), timeZoneId = TimeZone.currentSystemDefault().id, ) @@ -77,7 +77,7 @@ fun ChangeHistoryScreen( ChangeHistoryFieldEnum.CLASS_TEACHER_REMOVED, ChangeHistoryFieldEnum.CLASS_STUDENT_REMOVED -> - "${change.field.displayName}: \"${change.oldVal}\"" + "${change.field.displayName}: \"${change.newVal}\"" else -> stringResource( diff --git a/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/14.json b/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/14.json index 7f2a082eb..373818283 100644 --- a/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/14.json +++ b/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/14.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 14, - "identityHash": "4cdf3848a741fd847ca58eea4f4c6ec7", + "identityHash": "a22bc48539b22227055f09c7a3ac547e", "entities": [ { "tableName": "SchoolAppEntity", @@ -1736,7 +1736,7 @@ }, { "tableName": "ChangeHistoryEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hGuid` TEXT NOT NULL, `hGuidHash` INTEGER NOT NULL, `hTable` TEXT NOT NULL, `hTableGuid` TEXT NOT NULL, `hTimestamp` INTEGER NOT NULL, `hWhoGuid` TEXT NOT NULL, `hWhoGuidHash` INTEGER NOT NULL, `hLastModified` INTEGER NOT NULL, `hStored` INTEGER NOT NULL, PRIMARY KEY(`hGuidHash`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hGuid` TEXT NOT NULL, `hGuidHash` INTEGER NOT NULL, `hTable` TEXT NOT NULL, `hTableGuid` TEXT NOT NULL, `hWhoGuid` TEXT NOT NULL, `hWhoGuidHash` INTEGER NOT NULL, `hLastModified` INTEGER NOT NULL, `hStored` INTEGER NOT NULL, PRIMARY KEY(`hGuidHash`))", "fields": [ { "fieldPath": "hGuid", @@ -1762,12 +1762,6 @@ "affinity": "TEXT", "notNull": true }, - { - "fieldPath": "hTimestamp", - "columnName": "hTimestamp", - "affinity": "INTEGER", - "notNull": true - }, { "fieldPath": "hWhoGuid", "columnName": "hWhoGuid", @@ -1862,7 +1856,7 @@ ], "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, '4cdf3848a741fd847ca58eea4f4c6ec7')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a22bc48539b22227055f09c7a3ac547e')" ] } } \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabaseMigrations.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabaseMigrations.kt index 8b4d6f717..a12338d90 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabaseMigrations.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/RespectSchoolDatabaseMigrations.kt @@ -28,7 +28,6 @@ val MIGRATE_12_13 = object : Migration(12, 13) { `hGuid` TEXT NOT NULL, `hGuidHash` INTEGER NOT NULL, `hTable` TEXT NOT NULL, - `hTimestamp` INTEGER NOT NULL, `hWhoGuid` TEXT NOT NULL, `hWhoGuidHash` INTEGER NOT NULL, `hLastModified` INTEGER NOT NULL, diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt index f3b4faca1..37e105dbd 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt @@ -28,7 +28,6 @@ fun ChangeHistoryWithChanges.toModel(): ChangeHistoryEntry { return ChangeHistoryEntry( guid = history.hGuid, table = history.hTable, - timestamp = history.hTimestamp, whoGuid = history.hWhoGuid, tableGuid = history.hTableGuid, changes = changes.map { it.toModel() } @@ -56,7 +55,6 @@ fun ChangeHistoryEntry.toEntities( hGuid = guid, hGuidHash = guidHash, hTable = table, - hTimestamp = timestamp, hWhoGuid = whoGuid, hWhoGuidHash = uidNumberMapper(whoGuid), hTableGuid = tableGuid @@ -103,7 +101,6 @@ fun generateClassChanges( table = ChangeHistoryTableEnum.CLASS, tableGuid = hTableGuid, whoGuid = whoGuid, - timestamp = timestamp, changes = changes ) } @@ -113,18 +110,15 @@ private fun roleChangeField( oldRole: EnrollmentRoleEnum?, newRole: EnrollmentRoleEnum ): ChangeHistoryFieldEnum? { - return when { - oldRole == EnrollmentRoleEnum.PENDING_STUDENT && newRole == EnrollmentRoleEnum.STUDENT -> + return when (oldRole) { + EnrollmentRoleEnum.PENDING_STUDENT if newRole == EnrollmentRoleEnum.STUDENT -> ChangeHistoryFieldEnum.JOIN_REQUEST_APPROVED - - oldRole == EnrollmentRoleEnum.PENDING_TEACHER && newRole == EnrollmentRoleEnum.TEACHER -> + EnrollmentRoleEnum.PENDING_TEACHER if newRole == EnrollmentRoleEnum.TEACHER -> ChangeHistoryFieldEnum.JOIN_REQUEST_APPROVED - else -> null } } - fun generateEnrollmentChanges( hGuid: String, old: Enrollment?, @@ -150,15 +144,15 @@ fun generateEnrollmentChanges( ) } else { - if (new.removedAt != null && old.removedAt == null) { + if (new.status == StatusEnum.TO_BE_DELETED && old.status == StatusEnum.ACTIVE) { changes += ChangeHistoryChange( field = when (new.role) { EnrollmentRoleEnum.TEACHER -> ChangeHistoryFieldEnum.CLASS_TEACHER_REMOVED EnrollmentRoleEnum.STUDENT -> ChangeHistoryFieldEnum.CLASS_STUDENT_REMOVED else -> ChangeHistoryFieldEnum.CLASS_STUDENT_REMOVED }, - oldVal = name, - newVal = "" + oldVal = "", + newVal = name ) return ChangeHistoryEntry( @@ -166,7 +160,6 @@ fun generateEnrollmentChanges( table = ChangeHistoryTableEnum.CLASS, tableGuid = hTableGuid, whoGuid = whoGuid, - timestamp = timestamp, changes = changes ) } @@ -183,7 +176,6 @@ fun generateEnrollmentChanges( table = ChangeHistoryTableEnum.CLASS, tableGuid = hTableGuid, whoGuid = whoGuid, - timestamp = timestamp, changes = changes ) } @@ -201,7 +193,6 @@ fun generateEnrollmentChanges( table = ChangeHistoryTableEnum.ENROLLMENT, tableGuid = hTableGuid, whoGuid = whoGuid, - timestamp = timestamp, changes = changes ) } @@ -229,7 +220,6 @@ fun generatePersonChanges( return ChangeHistoryEntry( guid = primaryKeyGenerator.nextId(ChangeHistoryEntry.TABLE_ID).toString(), table = ChangeHistoryTableEnum.PERSON, - timestamp = timestamp, whoGuid = whoGuid, changes = changes, tableGuid = hTableGuid diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt index ac7ab3ff5..2d3ea8ad1 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/daos/ChangeHistoryDao.kt @@ -73,7 +73,7 @@ interface ChangeHistoryDao { FROM ChangeHistoryEntity WHERE (:table IS NULL OR hTable = :table) AND (:whoGuidHash = 0 OR hWhoGuidHash = :whoGuidHash) - ORDER BY hTimestamp DESC + ORDER BY hLastModified DESC """) fun listAsPagingSource( table: ChangeHistoryTableEnum?, @@ -103,6 +103,7 @@ interface ChangeHistoryDao { SELECT * FROM ChangeHistoryEntity WHERE hTableGuid = :guid + ORDER BY hLastModified DESC """) fun findByGuidAsFlow(guid: String): Flow> diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryEntity.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryEntity.kt index 8d74e93e2..c5eabc7e2 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryEntity.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ChangeHistoryEntity.kt @@ -13,7 +13,6 @@ data class ChangeHistoryEntity( val hGuidHash: Long, val hTable: ChangeHistoryTableEnum, val hTableGuid: String, - val hTimestamp: Long, val hWhoGuid: String, val hWhoGuidHash: Long, val hLastModified: Instant = Clock.System.now(), diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/SchoolDataSourceHttp.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/SchoolDataSourceHttp.kt index 468e0ce8b..9fd78a2bf 100644 --- a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/SchoolDataSourceHttp.kt +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/SchoolDataSourceHttp.kt @@ -142,6 +142,8 @@ class SchoolDataSourceHttp( httpClient = httpClient, tokenProvider = tokenProvider, validationHelper = validationHelper, + changeHistoryProvider = changeHistoryProvider, + markSentToServer = markSentToServer, ) } diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/EnrollmentDataSourceHttp.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/EnrollmentDataSourceHttp.kt index 44e5f74a0..8a71fe7c2 100644 --- a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/EnrollmentDataSourceHttp.kt +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/EnrollmentDataSourceHttp.kt @@ -1,8 +1,6 @@ package world.respect.datalayer.http.school -import io.github.aakira.napier.Napier import io.ktor.client.HttpClient -import io.ktor.client.request.delete import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.http.ContentType @@ -13,6 +11,8 @@ import io.ktor.util.reflect.typeInfo import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import world.respect.datalayer.AuthTokenProvider +import world.respect.datalayer.ChangeHistoryMarkSentToServer +import world.respect.datalayer.ChangeHistoryProvider import world.respect.datalayer.DataLayerParams import world.respect.datalayer.DataLoadParams import world.respect.datalayer.DataLoadState @@ -27,9 +27,9 @@ import world.respect.datalayer.http.ext.respectEndpointUrl import world.respect.datalayer.http.shared.paging.OffsetLimitHttpPagingSource import world.respect.datalayer.networkvalidation.ExtendedDataSourceValidationHelper import world.respect.datalayer.school.EnrollmentDataSource +import world.respect.datalayer.school.model.ChangeHistoryTableEnum import world.respect.datalayer.school.model.Enrollment import world.respect.datalayer.schooldirectory.SchoolDirectoryEntryDataSource -import world.respect.datalayer.shared.DataLayerTags.TAG_DATALAYER import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.params.GetListCommonParams @@ -39,6 +39,8 @@ class EnrollmentDataSourceHttp( private val httpClient: HttpClient, private val tokenProvider: AuthTokenProvider, private val validationHelper: ExtendedDataSourceValidationHelper?, + private val changeHistoryProvider: ChangeHistoryProvider, + private val markSentToServer: ChangeHistoryMarkSentToServer, ): EnrollmentDataSource, SchoolUrlBasedDataSource { private suspend fun EnrollmentDataSource.GetListParams.urlWithParams(): Url { @@ -122,15 +124,29 @@ class EnrollmentDataSourceHttp( } override suspend fun store(list: List) { - val url = respectEndpointUrl(EnrollmentDataSource.ENDPOINT_NAME) - val response = httpClient.post(url) { + + val changeHistories = changeHistoryProvider.getChangeHistoryEntries( + tableId = ChangeHistoryTableEnum.ENROLLMENT, + uids = list.map { it.uid } + ) + httpClient.post( + url = respectEndpointUrl(EnrollmentDataSource.ENDPOINT_NAME) + ) { useTokenProvider(tokenProvider) contentType(ContentType.Application.Json) - setBody(list) - } - Napier.d(tag = TAG_DATALAYER) { - "EnrollmentDataSourceHttp: posted ${list.size} items(${list.joinToString { it.uid }}) to $url (status=${response.status.value}" + if (changeHistories.isEmpty()) { + setBody(list) + } else { + setBody>( + DataAndChangeHistory( + data = list, + changeHistories = changeHistories + ) + ) + } } + markSentToServer.markSentToServer(changeHistories) + } } \ No newline at end of file diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt index 05ff408de..fc7c17c67 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt @@ -11,7 +11,6 @@ data class ChangeHistoryEntry( val guid: String, val tableGuid: String, val table: ChangeHistoryTableEnum, - val timestamp: Long, val whoGuid: String, val changes: List, override val lastModified: InstantAsISO8601 = Clock.System.now(), diff --git a/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/EnrollmentRoute.kt b/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/EnrollmentRoute.kt index e3db23789..27151350c 100644 --- a/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/EnrollmentRoute.kt +++ b/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/EnrollmentRoute.kt @@ -9,10 +9,17 @@ import io.ktor.server.response.respond import io.ktor.server.routing.Route import io.ktor.server.routing.get import io.ktor.server.routing.post +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromJsonElement import world.respect.datalayer.DataLoadParams import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.http.school.DataAndChangeHistory import world.respect.datalayer.school.EnrollmentDataSource import world.respect.datalayer.school.domain.GetPermissionLastModifiedUseCase +import world.respect.datalayer.school.model.Enrollment import world.respect.server.util.ext.offsetLimitPagingLoadParams import world.respect.server.util.ext.requireAccountScope import world.respect.server.util.ext.respondOffsetLimitPaging @@ -38,9 +45,32 @@ fun Route.EnrollmentRoute( } post(EnrollmentDataSource.ENDPOINT_NAME) { - schoolDataSource(call).enrollmentDataSource.store( - list = call.receive() - ) + val schoolDataSource = schoolDataSource(call) + + when (val incoming = call.receive()) { + + is JsonArray -> { + val enrollments = Json.decodeFromJsonElement>(incoming) + + schoolDataSource.enrollmentDataSource.store(enrollments) + } + + is JsonObject -> { + val request = Json.decodeFromJsonElement>(incoming) + + schoolDataSource.enrollmentDataSource.store(request.data) + + if (request.changeHistories.isNotEmpty()) { + schoolDataSource.changeHistoryDataSource.store(request.changeHistories) + schoolDataSource.changeHistoryDataSource.markSentToServer(request.changeHistories) + } + } + + else -> { + throw IllegalArgumentException("Invalid request format") + } + } + call.respond(HttpStatusCode.NoContent) } } \ No newline at end of file From f98536eccf4c213bd08bb0c7836c33dad9d6c54c Mon Sep 17 00:00:00 2001 From: lenovo Date: Thu, 26 Mar 2026 15:30:05 +0530 Subject: [PATCH 18/21] commit --- .../001_001_invite_users_using_qr_code_or_link_test.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml index 7dba8c6b8..154863658 100644 --- a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml +++ b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml @@ -231,6 +231,10 @@ onFlowComplete: id: "app_title" text: "Change History" - assertVisible: 'Person name changed from "TeacherA User" to "TeacherB User"' +- back +- back +- tapOn: "Classes" +- tapOn: "New Class" # E) Teacher share invite link for parent device based signup - tapOn: "Add student" From da0a66d5aa17544340c5357fd6bffff75b0c9321 Mon Sep 17 00:00:00 2001 From: lenovo Date: Fri, 27 Mar 2026 13:41:48 +0530 Subject: [PATCH 19/21] commit --- .../apps/changehistory/ChangeHistoryScreen.kt | 2 +- .../school/adapters/ChangeHistoryAdapter.kt | 47 ++++++++--- .../school/model/ChangeHistoryEntry.kt | 8 +- .../datalayer/school/model/findDifference.kt | 8 +- .../changehistory/ChangeHistoryViewModel.kt | 83 ++++++------------- 5 files changed, 72 insertions(+), 76 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt index 7eeae15ad..3a6215402 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/changehistory/ChangeHistoryScreen.kt @@ -58,7 +58,7 @@ fun ChangeHistoryScreen( text = group.person.fullName(), ) val createdAtStr = rememberFormattedDateTime( - timeInMillis = entry.lastModified.toEpochMilliseconds(), + timeInMillis = change.lastModified.toEpochMilliseconds(), timeZoneId = TimeZone.currentSystemDefault().id, ) diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt index 37e105dbd..4513d3c3a 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ChangeHistoryAdapter.kt @@ -16,6 +16,7 @@ import world.respect.datalayer.school.model.Person import world.respect.datalayer.school.model.StatusEnum import world.respect.datalayer.school.model.findDifference import world.respect.lib.primarykeygen.PrimaryKeyGenerator +import kotlin.time.Instant data class ChangeHistoryEntities( @@ -30,6 +31,8 @@ fun ChangeHistoryWithChanges.toModel(): ChangeHistoryEntry { table = history.hTable, whoGuid = history.hWhoGuid, tableGuid = history.hTableGuid, + lastModified = history.hLastModified, + stored = history.hStored, changes = changes.map { it.toModel() } ) } @@ -41,7 +44,9 @@ fun ChangeHistoryChangeEntity.toModel(): ChangeHistoryChange { field = hcField, oldVal = hcOldVal, newVal = hcNewVal, - synced = hcSynced + synced = hcSynced, + lastModified = hcLastModified, + stored = hcStored ) } @@ -56,6 +61,7 @@ fun ChangeHistoryEntry.toEntities( hGuidHash = guidHash, hTable = table, hWhoGuid = whoGuid, + hLastModified = lastModified, hWhoGuidHash = uidNumberMapper(whoGuid), hTableGuid = tableGuid ) @@ -66,6 +72,7 @@ fun ChangeHistoryEntry.toEntities( hcHistoryGuidHash = guidHash, hcField = change.field, hcOldVal = change.oldVal, + hcLastModified= change.lastModified, hcNewVal = change.newVal, hcSynced = change.synced ) @@ -84,6 +91,7 @@ fun generateClassChanges( timestamp: Long, hTableGuid: String ): ChangeHistoryEntry? { + val now = Instant.fromEpochMilliseconds(timestamp) val changes = mutableListOf() @@ -101,7 +109,9 @@ fun generateClassChanges( table = ChangeHistoryTableEnum.CLASS, tableGuid = hTableGuid, whoGuid = whoGuid, - changes = changes + changes = changes, + lastModified = now, + stored = now ) } @@ -128,6 +138,7 @@ fun generateEnrollmentChanges( hTableGuid: String, personName: String? ): ChangeHistoryEntry? { + val now = Instant.fromEpochMilliseconds(timestamp) val changes = mutableListOf() val name = personName ?: new.personUid @@ -140,7 +151,9 @@ fun generateEnrollmentChanges( else -> ChangeHistoryFieldEnum.CLASS_STUDENT_ADDED }, oldVal = null, - newVal = name + newVal = name, + lastModified = now, + stored = now ) } else { @@ -152,7 +165,9 @@ fun generateEnrollmentChanges( else -> ChangeHistoryFieldEnum.CLASS_STUDENT_REMOVED }, oldVal = "", - newVal = name + newVal = name, + lastModified = now, + stored = now ) return ChangeHistoryEntry( @@ -160,7 +175,9 @@ fun generateEnrollmentChanges( table = ChangeHistoryTableEnum.CLASS, tableGuid = hTableGuid, whoGuid = whoGuid, - changes = changes + changes = changes, + lastModified = now, + stored = now ) } @@ -168,7 +185,9 @@ fun generateEnrollmentChanges( changes += ChangeHistoryChange( field = it, oldVal = name, - newVal = name + newVal = name, + lastModified = now, + stored = now ) return ChangeHistoryEntry( @@ -176,11 +195,13 @@ fun generateEnrollmentChanges( table = ChangeHistoryTableEnum.CLASS, tableGuid = hTableGuid, whoGuid = whoGuid, - changes = changes + changes = changes, + lastModified = now, + stored = now ) } - findDifference(ChangeHistoryFieldEnum.ENROLLMENT_ROLE, old.role, new.role, changes) + findDifference(ChangeHistoryFieldEnum.ENROLLMENT_ROLE, old.role, new.role, changes,now) findDifference(ChangeHistoryFieldEnum.ENROLLMENT_BEGIN_DATE, old.beginDate, new.beginDate, changes) findDifference(ChangeHistoryFieldEnum.ENROLLMENT_END_DATE, old.endDate, new.endDate, changes) findDifference(ChangeHistoryFieldEnum.ENROLLMENT_STATUS, old.status, new.status, changes) @@ -193,7 +214,9 @@ fun generateEnrollmentChanges( table = ChangeHistoryTableEnum.ENROLLMENT, tableGuid = hTableGuid, whoGuid = whoGuid, - changes = changes + changes = changes, + lastModified = now, + stored = now ) } @@ -205,7 +228,7 @@ fun generatePersonChanges( timestamp: Long, hTableGuid: String ): ChangeHistoryEntry? { - + val now = Instant.fromEpochMilliseconds(timestamp) val changes = mutableListOf() findDifference(ChangeHistoryFieldEnum.PERSON_GIVEN_NAME, old?.fullName(), new.fullName(), changes) @@ -222,6 +245,8 @@ fun generatePersonChanges( table = ChangeHistoryTableEnum.PERSON, whoGuid = whoGuid, changes = changes, - tableGuid = hTableGuid + tableGuid = hTableGuid, + lastModified = now, + stored = now ) } \ No newline at end of file diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt index fc7c17c67..c96000751 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/ChangeHistoryEntry.kt @@ -13,8 +13,8 @@ data class ChangeHistoryEntry( val table: ChangeHistoryTableEnum, val whoGuid: String, val changes: List, - override val lastModified: InstantAsISO8601 = Clock.System.now(), - override val stored: InstantAsISO8601 = Clock.System.now(), + override val lastModified: InstantAsISO8601 , + override val stored: InstantAsISO8601 , ): ModelWithTimes { companion object { @@ -38,6 +38,6 @@ data class ChangeHistoryChange( val newVal: String, val oldVal: String?, val synced : Boolean = false, - override val lastModified: InstantAsISO8601 = Clock.System.now(), - override val stored: InstantAsISO8601 = Clock.System.now(), + override val lastModified: InstantAsISO8601 , + override val stored: InstantAsISO8601 , ): ModelWithTimes \ No newline at end of file diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/findDifference.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/findDifference.kt index 44b151c76..f515d62d8 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/findDifference.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/findDifference.kt @@ -1,10 +1,14 @@ package world.respect.datalayer.school.model +import kotlin.time.Clock +import kotlin.time.Instant + fun findDifference( field: ChangeHistoryFieldEnum, oldVal: T?, newVal: T?, - changes: MutableList + changes: MutableList, + now: Instant = Clock.System.now(), ) { val oldString = oldVal?.toString() val newString = newVal?.toString() @@ -15,6 +19,8 @@ fun findDifference( field = field, oldVal = oldString, newVal = newString ?: "", + lastModified = now, + stored = now ) ) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryViewModel.kt index c5258fe2f..32ac4c36b 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryViewModel.kt @@ -51,61 +51,31 @@ class ChangeHistoryViewModel( init { viewModelScope.launch { - when (route.tableValue) { - ChangeHistoryTableEnum.PERSON -> { - val changeHistoryFlow = - schoolDataSource.changeHistoryDataSource - .findByGuidAsFlow(route.guid) - .shareIn(viewModelScope, SharingStarted.Lazily, replay = 1) + val changeHistoryFlow = + schoolDataSource.changeHistoryDataSource + .findByGuidAsFlow(route.guid) + .shareIn(viewModelScope, SharingStarted.Lazily, replay = 1) - viewModelScope.launch { + changeHistoryFlow.collect { state -> - changeHistoryFlow.collect { state -> - - val changeHistoryList = state.dataOrNull() ?: emptyList() - - val grouped = changeHistoryList.groupBy { it.whoGuid } - - val resultList = grouped.mapNotNull { (whoGuid, entries) -> - - val person = schoolDataSource.personDataSource.findByGuid( - loadParams = DataLoadParams(), - guid = whoGuid - ).dataOrNull() - - person?.let { - ChangeHistoryEntryWithWhoDid( - person = it, - changeHistoryEntry = entries - ) - } - } - - _uiState.update { prev -> - prev.copy( - changeHistoryEntryWithWhoDid = resultList - ) - } - } - } - } - ChangeHistoryTableEnum.CLASS -> { - - val changeHistoryList = schoolDataSource.changeHistoryDataSource - .findByGuid( - loadParams = DataLoadParams(), - guid = route.guid, - ).dataOrNull() ?: return@launch + val changeHistoryList = state.dataOrNull()?.map { entry -> + entry.copy( + changes = entry.changes.sortedByDescending { it.lastModified } + ) + }?.sortedByDescending { it.lastModified } - val whoGuids = changeHistoryList.groupBy { it.whoGuid } + val grouped = + changeHistoryList?.groupBy { it.whoGuid } - val resultList = whoGuids.mapNotNull { (whoGuid, entries) -> + val resultList = + grouped?.mapNotNull { (whoGuid, entries) -> - val person = schoolDataSource.personDataSource.findByGuid( - loadParams = DataLoadParams(), - guid = whoGuid, - ).dataOrNull() + val person = + schoolDataSource.personDataSource.findByGuid( + loadParams = DataLoadParams(), + guid = whoGuid + ).dataOrNull() person?.let { ChangeHistoryEntryWithWhoDid( @@ -115,19 +85,14 @@ class ChangeHistoryViewModel( } } - _uiState.update { prev -> - prev.copy( - changeHistoryEntryWithWhoDid = resultList - ) - } - } - - ChangeHistoryTableEnum.ENROLLMENT -> { - + _uiState.update { previous -> + previous.copy( + changeHistoryEntryWithWhoDid = resultList + ) } } - } + _appUiState.update { prev -> prev.copy( title = Res.string.change_history.asUiText(), From f5dddb913dc8b0473c277b0c17d5e0b1a2cc86af Mon Sep 17 00:00:00 2001 From: lenovo Date: Tue, 31 Mar 2026 16:28:21 +0530 Subject: [PATCH 20/21] commit --- .../view/clazz/detail/ClazzDetailScreen.kt | 16 +++---- .../view/person/detail/PersonDetailScreen.kt | 12 +++--- .../school/ChangeHistoryMarkSentToServerDb.kt | 5 +-- .../db/school/ChangeHistoryProviderDb.kt | 2 +- .../http/school/ClassDataSourceHttp.kt | 2 +- .../http/school/EnrollmentDataSourceHttp.kt | 2 +- .../http/school/PersonDataSourceHttp.kt | 4 +- .../datalayer/ChangeHistoryProvider.kt | 2 +- .../domain/CheckPersonPermissionUseCase.kt | 6 --- .../changehistory/ChangeHistoryViewModel.kt | 16 +++---- .../clazz/detail/ClazzDetailViewModel.kt | 7 --- .../person/detail/PersonDetailViewModel.kt | 8 +--- .../routes/school/respect/ClassRoute.kt | 43 ++++++------------- .../routes/school/respect/EnrollmentRoute.kt | 43 ++++++------------- .../routes/school/respect/PersonRoute.kt | 39 ++++++----------- .../util/ext/ReceiveDataAndChangeHistory.kt | 36 ++++++++++++++++ 16 files changed, 103 insertions(+), 140 deletions(-) create mode 100644 respect-server/src/main/kotlin/world/respect/server/util/ext/ReceiveDataAndChangeHistory.kt diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 4e9f7010b..7782cfd2f 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -148,15 +148,13 @@ fun ClazzDetailScreen( ) } item { - if(uiState.changeHistoryButtonVisible){ - HorizontalDivider() - RespectQuickActionButton( - labelText = stringResource(Res.string.change_history), - imageVector = Icons.Default.Replay, - onClick = onClickChangeHistoryButton, - ) - HorizontalDivider() - } + HorizontalDivider() + RespectQuickActionButton( + labelText = stringResource(Res.string.change_history), + imageVector = Icons.Default.Replay, + onClick = onClickChangeHistoryButton, + ) + HorizontalDivider() } diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/person/detail/PersonDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/person/detail/PersonDetailScreen.kt index 256135a91..3851f755c 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/person/detail/PersonDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/person/detail/PersonDetailScreen.kt @@ -84,13 +84,11 @@ fun PersonDetailScreen( onClick = onClickCreateAccount, ) } - if(uiState.changeHistoryButtonVisible){ - RespectQuickActionButton( - labelText = stringResource(Res.string.change_history), - imageVector = Icons.Default.Replay, - onClick = onClickChangeHistoryButton, - ) - } + RespectQuickActionButton( + labelText = stringResource(Res.string.change_history), + imageVector = Icons.Default.Replay, + onClick = onClickChangeHistoryButton, + ) } HorizontalDivider() diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryMarkSentToServerDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryMarkSentToServerDb.kt index b638c2c72..b46ca7c44 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryMarkSentToServerDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryMarkSentToServerDb.kt @@ -18,7 +18,6 @@ class ChangeHistoryMarkSentToServerDb( if (historyGuidHashes.isEmpty()) return - schoolDb.getChangeHistoryDao().markByHistoryGuids(historyGuidHashes) } - - + schoolDb.getChangeHistoryDao().markByHistoryGuids(historyGuidHashes) + } } \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryProviderDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryProviderDb.kt index 28c56cfaf..5e465d001 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryProviderDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/ChangeHistoryProviderDb.kt @@ -10,7 +10,7 @@ class ChangeHistoryProviderDb( private val schoolDb: RespectSchoolDatabase, ) : ChangeHistoryProvider { - override suspend fun getChangeHistoryEntries( + override suspend fun getPendingChangeHistoryEntries( tableId: ChangeHistoryTableEnum, uids: List ): List { diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ClassDataSourceHttp.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ClassDataSourceHttp.kt index fd9a4649d..08006e2e5 100644 --- a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ClassDataSourceHttp.kt +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/ClassDataSourceHttp.kt @@ -113,7 +113,7 @@ class ClassDataSourceHttp( } override suspend fun store(list: List) { - val changeHistories = changeHistoryProvider.getChangeHistoryEntries( + val changeHistories = changeHistoryProvider.getPendingChangeHistoryEntries( tableId = ChangeHistoryTableEnum.CLASS, uids = list.map { it.guid } ) diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/EnrollmentDataSourceHttp.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/EnrollmentDataSourceHttp.kt index 8a71fe7c2..4f269dc40 100644 --- a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/EnrollmentDataSourceHttp.kt +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/EnrollmentDataSourceHttp.kt @@ -125,7 +125,7 @@ class EnrollmentDataSourceHttp( override suspend fun store(list: List) { - val changeHistories = changeHistoryProvider.getChangeHistoryEntries( + val changeHistories = changeHistoryProvider.getPendingChangeHistoryEntries( tableId = ChangeHistoryTableEnum.ENROLLMENT, uids = list.map { it.uid } ) diff --git a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/PersonDataSourceHttp.kt b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/PersonDataSourceHttp.kt index e80b4ef0b..6e839bf8d 100644 --- a/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/PersonDataSourceHttp.kt +++ b/respect-datalayer-http/src/commonMain/kotlin/world/respect/datalayer/http/school/PersonDataSourceHttp.kt @@ -28,10 +28,8 @@ import world.respect.datalayer.http.shared.paging.OffsetLimitHttpPagingSource import world.respect.datalayer.networkvalidation.ExtendedDataSourceValidationHelper import world.respect.datalayer.school.PersonDataSource import world.respect.datalayer.school.adapters.asListDetails -import world.respect.datalayer.school.model.ChangeHistoryEntry import world.respect.datalayer.school.model.ChangeHistoryTableEnum import world.respect.datalayer.school.model.Person -import world.respect.datalayer.school.model.PersonWithHistoryRequest import world.respect.datalayer.school.model.composites.PersonListDetails import world.respect.datalayer.schooldirectory.SchoolDirectoryEntryDataSource import world.respect.datalayer.shared.paging.IPagingSourceFactory @@ -161,7 +159,7 @@ class PersonDataSourceHttp( override suspend fun store(list: List) { - val changeHistories = changeHistoryProvider.getChangeHistoryEntries( + val changeHistories = changeHistoryProvider.getPendingChangeHistoryEntries( tableId = ChangeHistoryTableEnum.PERSON, uids = list.map { it.guid } ) diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/ChangeHistoryProvider.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/ChangeHistoryProvider.kt index 38e1e1853..155ea4e90 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/ChangeHistoryProvider.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/ChangeHistoryProvider.kt @@ -4,7 +4,7 @@ import world.respect.datalayer.school.model.ChangeHistoryEntry import world.respect.datalayer.school.model.ChangeHistoryTableEnum interface ChangeHistoryProvider { - suspend fun getChangeHistoryEntries( + suspend fun getPendingChangeHistoryEntries( tableId: ChangeHistoryTableEnum, uids: List ): List diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/domain/CheckPersonPermissionUseCase.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/domain/CheckPersonPermissionUseCase.kt index c0531f7e9..6f251f008 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/domain/CheckPersonPermissionUseCase.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/domain/CheckPersonPermissionUseCase.kt @@ -24,12 +24,6 @@ interface CheckPersonPermissionUseCase { ) companion object { - val READ_PERMISSIONS = PermissionsRequiredByRole( - roleAdminPermissionRequired = PermissionFlags.SYSTEM_ADMIN, - roleTeacherPermissionRequired = PermissionFlags.PERSON_TEACHER_READ, - roleStudentPermissionRequired = PermissionFlags.PERSON_STUDENT_READ, - roleParentPermissionRequired = PermissionFlags.PERSON_PARENT_READ, - ) val WRITE_PERMISSIONS = PermissionsRequiredByRole( roleAdminPermissionRequired = PermissionFlags.SYSTEM_ADMIN, roleTeacherPermissionRequired = PermissionFlags.PERSON_TEACHER_WRITE, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryViewModel.kt index 32ac4c36b..e4b5b3ce8 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/changehistory/ChangeHistoryViewModel.kt @@ -35,10 +35,11 @@ class ChangeHistoryViewModel( savedStateHandle: SavedStateHandle, accountManager: RespectAccountManager ) : RespectViewModel(savedStateHandle), KoinScopeComponent { - private val schoolDataSource: SchoolDataSource by inject() override val scope: Scope = accountManager.requireActiveAccountScope() + private val schoolDataSource: SchoolDataSource by inject() + private val route: ChangeHistory = savedStateHandle.toRoute() private val _uiState = MutableStateFlow( @@ -59,17 +60,11 @@ class ChangeHistoryViewModel( changeHistoryFlow.collect { state -> - val changeHistoryList = state.dataOrNull()?.map { entry -> - entry.copy( - changes = entry.changes.sortedByDescending { it.lastModified } - ) - }?.sortedByDescending { it.lastModified } + val changeHistoryList = state.dataOrNull().orEmpty() - val grouped = - changeHistoryList?.groupBy { it.whoGuid } + val groupedByWho = changeHistoryList.groupBy { it.whoGuid } - val resultList = - grouped?.mapNotNull { (whoGuid, entries) -> + val resultList = groupedByWho.mapNotNull { (whoGuid, entries) -> val person = schoolDataSource.personDataSource.findByGuid( @@ -92,7 +87,6 @@ class ChangeHistoryViewModel( } } } - _appUiState.update { prev -> prev.copy( title = Res.string.change_history.asUiText(), diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 64b1c7a79..dc664b75a 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -88,7 +88,6 @@ data class ClazzDetailUiState( val inviteCodePrefix: String? = null, val showAddStudent: Boolean = false, val showAddTeacher: Boolean = false, - val changeHistoryButtonVisible: Boolean = false, ) class ClazzDetailViewModel( @@ -186,16 +185,10 @@ class ClazzDetailViewModel( viewModelScope.launch { _uiState.whenSubscribed { accountManager.selectedAccountAndPersonFlow.collect { selectedAccountAndPerson -> - val hasReadPermission = checkPersonPermissionUseCase( - otherPersonUid = selectedAccountAndPerson?.person?.guid?:"", - otherPersonKnownRole = null, - permissionsRequiredByRole = CheckPersonPermissionUseCase.PermissionsRequiredByRole.READ_PERMISSIONS, - ) _uiState.update { prev -> prev.copy( showAddStudent = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true, showAddTeacher = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true, - changeHistoryButtonVisible = hasReadPermission, ) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/detail/PersonDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/detail/PersonDetailViewModel.kt index e15cced9a..22c5c6311 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/detail/PersonDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/detail/PersonDetailViewModel.kt @@ -43,7 +43,6 @@ data class PersonDetailUiState( val persons: DataLoadState> = DataLoadingState(), val manageAccountVisible: Boolean = false, val createAccountVisible: Boolean = false, - val changeHistoryButtonVisible: Boolean = false, ) { val person: Person? @@ -106,11 +105,7 @@ class PersonDetailViewModel( otherPersonKnownRole = null, permissionsRequiredByRole = CheckPersonPermissionUseCase.PermissionsRequiredByRole.WRITE_PERMISSIONS, ) - val hasReadPermission = checkPersonPermissionUseCase( - otherPersonUid = route.guid, - otherPersonKnownRole = null, - permissionsRequiredByRole = CheckPersonPermissionUseCase.PermissionsRequiredByRole.READ_PERMISSIONS, - ) + _appUiState.update { prev -> prev.copy( title = personVal?.fullName()?.asUiText(), @@ -124,7 +119,6 @@ class PersonDetailViewModel( prev.copy( persons = persons, manageAccountVisible = hasAccountPermission && personVal?.username != null, - changeHistoryButtonVisible = hasReadPermission, createAccountVisible = personVal != null && activeAccount?.person?.isAdminOrTeacher() == true && personVal.username == null, diff --git a/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/ClassRoute.kt b/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/ClassRoute.kt index 1c94dccd5..2ec04f7e6 100644 --- a/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/ClassRoute.kt +++ b/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/ClassRoute.kt @@ -1,25 +1,21 @@ package world.respect.server.routes.school.respect import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall -import io.ktor.server.request.receive import io.ktor.server.response.header -import io.ktor.server.response.respond import io.ktor.server.routing.Route import io.ktor.server.routing.get import io.ktor.server.routing.post +import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.decodeFromJsonElement +import org.koin.ktor.ext.inject import world.respect.datalayer.DataLoadParams import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.http.school.DataAndChangeHistory import world.respect.datalayer.school.ClassDataSource import world.respect.datalayer.school.model.Clazz import world.respect.server.util.ext.offsetLimitPagingLoadParams +import world.respect.server.util.ext.receiveDataAndChangeHistory import world.respect.server.util.ext.requireAccountScope import world.respect.server.util.ext.respondOffsetLimitPaging @@ -29,6 +25,8 @@ fun Route.ClassRoute( call.requireAccountScope().get() }, ) { + val json:Json by inject() + get(ClassDataSource.ENDPOINT_NAME) { call.response.header(HttpHeaders.Vary, HttpHeaders.Authorization) call.respondOffsetLimitPaging( @@ -45,31 +43,18 @@ fun Route.ClassRoute( post(ClassDataSource.ENDPOINT_NAME) { val schoolDataSource = schoolDataSource(call) - when (val incoming = call.receive()) { - - is JsonArray -> { - val clazz = Json.decodeFromJsonElement>(incoming) - - schoolDataSource.classDataSource.store(clazz) - } - - is JsonObject -> { - val request = Json.decodeFromJsonElement>(incoming) - - schoolDataSource.classDataSource.store(request.data) + val request = call.receiveDataAndChangeHistory( + json, + ListSerializer(Clazz.serializer()), + DataAndChangeHistory.serializer(Clazz.serializer()) + ) - if (request.changeHistories.isNotEmpty()) { - schoolDataSource.changeHistoryDataSource.store(request.changeHistories) - schoolDataSource.changeHistoryDataSource.markSentToServer(request.changeHistories) - } - } + schoolDataSource.classDataSource.store(request.data) - else -> { - throw IllegalArgumentException("Invalid request format") - } + if (request.changeHistories.isNotEmpty()) { + schoolDataSource.changeHistoryDataSource.store(request.changeHistories) + schoolDataSource.changeHistoryDataSource.markSentToServer(request.changeHistories) } - - call.respond(HttpStatusCode.NoContent) } } \ No newline at end of file diff --git a/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/EnrollmentRoute.kt b/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/EnrollmentRoute.kt index 27151350c..e18bce205 100644 --- a/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/EnrollmentRoute.kt +++ b/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/EnrollmentRoute.kt @@ -1,19 +1,14 @@ package world.respect.server.routes.school.respect import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall -import io.ktor.server.request.receive import io.ktor.server.response.header -import io.ktor.server.response.respond import io.ktor.server.routing.Route import io.ktor.server.routing.get import io.ktor.server.routing.post +import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.decodeFromJsonElement +import org.koin.ktor.ext.inject import world.respect.datalayer.DataLoadParams import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.http.school.DataAndChangeHistory @@ -21,6 +16,7 @@ import world.respect.datalayer.school.EnrollmentDataSource import world.respect.datalayer.school.domain.GetPermissionLastModifiedUseCase import world.respect.datalayer.school.model.Enrollment import world.respect.server.util.ext.offsetLimitPagingLoadParams +import world.respect.server.util.ext.receiveDataAndChangeHistory import world.respect.server.util.ext.requireAccountScope import world.respect.server.util.ext.respondOffsetLimitPaging @@ -32,6 +28,8 @@ fun Route.EnrollmentRoute( call.requireAccountScope().get() } ) { + val json:Json by inject() + get(EnrollmentDataSource.ENDPOINT_NAME) { call.response.header(HttpHeaders.Vary, HttpHeaders.Authorization) call.respondOffsetLimitPaging( @@ -47,30 +45,17 @@ fun Route.EnrollmentRoute( post(EnrollmentDataSource.ENDPOINT_NAME) { val schoolDataSource = schoolDataSource(call) - when (val incoming = call.receive()) { - - is JsonArray -> { - val enrollments = Json.decodeFromJsonElement>(incoming) - - schoolDataSource.enrollmentDataSource.store(enrollments) - } - - is JsonObject -> { - val request = Json.decodeFromJsonElement>(incoming) - - schoolDataSource.enrollmentDataSource.store(request.data) + val request = call.receiveDataAndChangeHistory( + json, + ListSerializer(Enrollment.serializer()), + DataAndChangeHistory.serializer(Enrollment.serializer()) + ) - if (request.changeHistories.isNotEmpty()) { - schoolDataSource.changeHistoryDataSource.store(request.changeHistories) - schoolDataSource.changeHistoryDataSource.markSentToServer(request.changeHistories) - } - } + schoolDataSource.enrollmentDataSource.store(request.data) - else -> { - throw IllegalArgumentException("Invalid request format") - } + if (request.changeHistories.isNotEmpty()) { + schoolDataSource.changeHistoryDataSource.store(request.changeHistories) + schoolDataSource.changeHistoryDataSource.markSentToServer(request.changeHistories) } - - call.respond(HttpStatusCode.NoContent) } } \ No newline at end of file diff --git a/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/PersonRoute.kt b/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/PersonRoute.kt index faada201c..8d68cf4ec 100644 --- a/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/PersonRoute.kt +++ b/respect-server/src/main/kotlin/world/respect/server/routes/school/respect/PersonRoute.kt @@ -3,23 +3,21 @@ package world.respect.server.routes.school.respect import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall -import io.ktor.server.request.receive import io.ktor.server.response.header import io.ktor.server.response.respond import io.ktor.server.routing.Route import io.ktor.server.routing.get import io.ktor.server.routing.post +import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.decodeFromJsonElement +import org.koin.ktor.ext.inject import world.respect.datalayer.DataLoadParams import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.http.school.DataAndChangeHistory import world.respect.datalayer.school.PersonDataSource import world.respect.datalayer.school.model.Person import world.respect.server.util.ext.offsetLimitPagingLoadParams +import world.respect.server.util.ext.receiveDataAndChangeHistory import world.respect.server.util.ext.requireAccountScope import world.respect.server.util.ext.respondOffsetLimitPaging @@ -29,6 +27,8 @@ fun Route.PersonRoute( call.requireAccountScope().get() }, ) { + val json:Json by inject() + get(PersonDataSource.ENDPOINT_NAME) { val schoolDataSource = schoolDataSource(call) call.response.header(HttpHeaders.Vary, HttpHeaders.Authorization) @@ -49,28 +49,17 @@ fun Route.PersonRoute( post(PersonDataSource.ENDPOINT_NAME) { val schoolDataSource = schoolDataSource(call) - when (val incoming = call.receive()) { - - is JsonArray -> { - val persons = Json.decodeFromJsonElement>(incoming) - - schoolDataSource.personDataSource.store(persons) - } - - is JsonObject -> { - val request = Json.decodeFromJsonElement>(incoming) - - schoolDataSource.personDataSource.store(request.data) + val request = call.receiveDataAndChangeHistory( + json, + ListSerializer(Person.serializer()), + DataAndChangeHistory.serializer(Person.serializer()) + ) - if (request.changeHistories.isNotEmpty()) { - schoolDataSource.changeHistoryDataSource.store(request.changeHistories) - schoolDataSource.changeHistoryDataSource.markSentToServer(request.changeHistories) - } - } + schoolDataSource.personDataSource.store(request.data) - else -> { - throw IllegalArgumentException("Invalid request format") - } + if (request.changeHistories.isNotEmpty()) { + schoolDataSource.changeHistoryDataSource.store(request.changeHistories) + schoolDataSource.changeHistoryDataSource.markSentToServer(request.changeHistories) } call.respond(HttpStatusCode.NoContent) diff --git a/respect-server/src/main/kotlin/world/respect/server/util/ext/ReceiveDataAndChangeHistory.kt b/respect-server/src/main/kotlin/world/respect/server/util/ext/ReceiveDataAndChangeHistory.kt new file mode 100644 index 000000000..350e560c1 --- /dev/null +++ b/respect-server/src/main/kotlin/world/respect/server/util/ext/ReceiveDataAndChangeHistory.kt @@ -0,0 +1,36 @@ +package world.respect.server.util.ext + +import io.ktor.server.application.ApplicationCall +import io.ktor.server.request.receive +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import world.respect.datalayer.http.school.DataAndChangeHistory + +suspend fun ApplicationCall.receiveDataAndChangeHistory( + json: Json, + listDeserializer: DeserializationStrategy>, + wrapperDeserializer: DeserializationStrategy> +): DataAndChangeHistory { + + return when (val incoming = receive()) { + + is JsonArray -> { + val data = json.decodeFromJsonElement(listDeserializer, incoming) + DataAndChangeHistory( + data = data, + changeHistories = emptyList() + ) + } + + is JsonObject -> { + json.decodeFromJsonElement(wrapperDeserializer, incoming) + } + + else -> { + throw IllegalArgumentException("Invalid request format") + } + } +} \ No newline at end of file From 0336cc04ab1ab16f1bf1ecbbc53d4d9771860d87 Mon Sep 17 00:00:00 2001 From: lenovo Date: Mon, 6 Apr 2026 16:58:46 +0530 Subject: [PATCH 21/21] commit --- .../school/PersonRepositoryIntegrationTest.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/respect-datalayer-repository/src/jvmTest/kotlin/world/respect/datalayer/repository/school/PersonRepositoryIntegrationTest.kt b/respect-datalayer-repository/src/jvmTest/kotlin/world/respect/datalayer/repository/school/PersonRepositoryIntegrationTest.kt index c73fadc13..8b979d296 100644 --- a/respect-datalayer-repository/src/jvmTest/kotlin/world/respect/datalayer/repository/school/PersonRepositoryIntegrationTest.kt +++ b/respect-datalayer-repository/src/jvmTest/kotlin/world/respect/datalayer/repository/school/PersonRepositoryIntegrationTest.kt @@ -7,8 +7,11 @@ import io.github.aakira.napier.Napier import io.ktor.server.routing.route import kotlinx.coroutines.flow.filter import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json import org.junit.Rule import org.junit.rules.TemporaryFolder +import org.koin.core.context.startKoin +import org.koin.dsl.module import world.respect.datalayer.DataLoadParams import world.respect.datalayer.DataReadyState import world.respect.datalayer.NoDataLoadedState @@ -299,7 +302,16 @@ class PersonRepositoryIntegrationTest { } server.start() - + startKoin { + modules(module { + single { + Json { + encodeDefaults = false + ignoreUnknownKeys = true + } + } + }) + } clients.first().insertServerAdminAndDefaultGrants() clients.first().schoolDataSource.personDataSource.store(