diff --git a/library/db/room-schemas/org.cru.godtools.db.room.GodToolsRoomDatabase/15.json b/library/db/room-schemas/org.cru.godtools.db.room.GodToolsRoomDatabase/15.json new file mode 100644 index 0000000000..07d793482e --- /dev/null +++ b/library/db/room-schemas/org.cru.godtools.db.room.GodToolsRoomDatabase/15.json @@ -0,0 +1,668 @@ +{ + "formatVersion": 1, + "database": { + "version": 15, + "identityHash": "9334f5fbc202b319d6b690c3c809c1ca", + "entities": [ + { + "tableName": "attachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `tool` TEXT, `filename` TEXT, `sha256` TEXT, `isDownloaded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`), FOREIGN KEY(`tool`) REFERENCES `tools`(`code`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tool", + "columnName": "tool", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sha256", + "columnName": "sha256", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDownloaded", + "columnName": "isDownloaded", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_attachments_tool", + "unique": false, + "columnNames": [ + "tool" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_attachments_tool` ON `${TABLE_NAME}` (`tool`)" + } + ], + "foreignKeys": [ + { + "table": "tools", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "tool" + ], + "referencedColumns": [ + "code" + ] + } + ] + }, + { + "tableName": "languages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`code` TEXT NOT NULL, `id` INTEGER NOT NULL, `name` TEXT, `isAdded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`code`))", + "fields": [ + { + "fieldPath": "code", + "columnName": "code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isAdded", + "columnName": "isAdded", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "code" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "downloadedFiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`filename` TEXT NOT NULL, PRIMARY KEY(`filename`))", + "fields": [ + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "filename" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "followups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `email` TEXT NOT NULL, `destination` INTEGER NOT NULL, `language` TEXT NOT NULL, `createdAt` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destination", + "columnName": "destination", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "global_activity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`users` INTEGER NOT NULL, `countries` INTEGER NOT NULL, `launches` INTEGER NOT NULL, `gospelPresentations` INTEGER NOT NULL, `id` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "users", + "columnName": "users", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "countries", + "columnName": "countries", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "launches", + "columnName": "launches", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gospelPresentations", + "columnName": "gospelPresentations", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tools", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `code` TEXT NOT NULL, `type` TEXT NOT NULL, `name` TEXT, `category` TEXT, `description` TEXT, `shares` INTEGER NOT NULL DEFAULT 0, `pendingShares` INTEGER NOT NULL DEFAULT 0, `bannerId` INTEGER, `detailsBannerId` INTEGER, `detailsBannerAnimationId` INTEGER, `detailsBannerYoutubeVideoId` TEXT, `isScreenShareDisabled` INTEGER NOT NULL DEFAULT false, `defaultOrder` INTEGER NOT NULL DEFAULT 0, `order` INTEGER NOT NULL DEFAULT 2147483647, `metatoolCode` TEXT, `defaultVariantCode` TEXT, `isFavorite` INTEGER NOT NULL DEFAULT false, `isHidden` INTEGER NOT NULL DEFAULT false, `isSpotlight` INTEGER NOT NULL DEFAULT false, `changedFields` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`code`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "code", + "columnName": "code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shares", + "columnName": "shares", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "pendingShares", + "columnName": "pendingShares", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "bannerId", + "columnName": "bannerId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "detailsBannerId", + "columnName": "detailsBannerId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "detailsBannerAnimationId", + "columnName": "detailsBannerAnimationId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "detailsBannerYoutubeVideoId", + "columnName": "detailsBannerYoutubeVideoId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isScreenShareDisabled", + "columnName": "isScreenShareDisabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "defaultOrder", + "columnName": "defaultOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "2147483647" + }, + { + "fieldPath": "metatoolCode", + "columnName": "metatoolCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "defaultVariantCode", + "columnName": "defaultVariantCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isSpotlight", + "columnName": "isSpotlight", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "changedFields", + "columnName": "changedFields", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "code" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "training_tips", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`isCompleted` INTEGER NOT NULL, `isNew` INTEGER NOT NULL, `tool` TEXT NOT NULL, `locale` TEXT NOT NULL, `tipId` TEXT NOT NULL, PRIMARY KEY(`tool`, `locale`, `tipId`))", + "fields": [ + { + "fieldPath": "isCompleted", + "columnName": "isCompleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNew", + "columnName": "isNew", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key.tool", + "columnName": "tool", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "key.locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "key.tipId", + "columnName": "tipId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "tool", + "locale", + "tipId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "translations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `tool` TEXT NOT NULL, `locale` TEXT NOT NULL, `version` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `tagline` TEXT, `toolDetailsConversationStarters` TEXT, `toolDetailsOutline` TEXT, `toolDetailsBibleReferences` TEXT, `manifestFileName` TEXT, `isDownloaded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`), FOREIGN KEY(`tool`) REFERENCES `tools`(`code`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`locale`) REFERENCES `languages`(`code`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tool", + "columnName": "tool", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tagline", + "columnName": "tagline", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toolDetailsConversationStarters", + "columnName": "toolDetailsConversationStarters", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toolDetailsOutline", + "columnName": "toolDetailsOutline", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toolDetailsBibleReferences", + "columnName": "toolDetailsBibleReferences", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "manifestFileName", + "columnName": "manifestFileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDownloaded", + "columnName": "isDownloaded", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_translations_tool_locale", + "unique": false, + "columnNames": [ + "tool", + "locale" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_translations_tool_locale` ON `${TABLE_NAME}` (`tool`, `locale`)" + }, + { + "name": "index_translations_tool_locale_version", + "unique": false, + "columnNames": [ + "tool", + "locale", + "version" + ], + "orders": [ + "ASC", + "ASC", + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_translations_tool_locale_version` ON `${TABLE_NAME}` (`tool` ASC, `locale` ASC, `version` DESC)" + } + ], + "foreignKeys": [ + { + "table": "tools", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "tool" + ], + "referencedColumns": [ + "code" + ] + }, + { + "table": "languages", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "locale" + ], + "referencedColumns": [ + "code" + ] + } + ] + }, + { + "tableName": "users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `ssoGuid` TEXT, `name` TEXT, `createdAt` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ssoGuid", + "columnName": "ssoGuid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "user_counters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `count` INTEGER NOT NULL, `decayedCount` REAL NOT NULL, `delta` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "decayedCount", + "columnName": "decayedCount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "delta", + "columnName": "delta", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "last_sync_times", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `time` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9334f5fbc202b319d6b690c3c809c1ca')" + ] + } +} \ No newline at end of file diff --git a/library/db/src/main/kotlin/org/cru/godtools/db/repository/ToolsRepository.kt b/library/db/src/main/kotlin/org/cru/godtools/db/repository/ToolsRepository.kt index d509531bd6..80b123f5d7 100644 --- a/library/db/src/main/kotlin/org/cru/godtools/db/repository/ToolsRepository.kt +++ b/library/db/src/main/kotlin/org/cru/godtools/db/repository/ToolsRepository.kt @@ -28,7 +28,7 @@ interface ToolsRepository { fun toolsChangeFlow(): Flow - suspend fun pinTool(code: String) + suspend fun pinTool(code: String, trackChanges: Boolean = true) suspend fun unpinTool(code: String) suspend fun storeToolOrder(tools: List) diff --git a/library/db/src/main/kotlin/org/cru/godtools/db/room/GodToolsRoomDatabase.kt b/library/db/src/main/kotlin/org/cru/godtools/db/room/GodToolsRoomDatabase.kt index cd4cc4a6c4..b0e94a0a3c 100644 --- a/library/db/src/main/kotlin/org/cru/godtools/db/room/GodToolsRoomDatabase.kt +++ b/library/db/src/main/kotlin/org/cru/godtools/db/room/GodToolsRoomDatabase.kt @@ -43,7 +43,7 @@ import org.cru.godtools.db.room.repository.UserCountersRoomRepository import org.cru.godtools.db.room.repository.UserRoomRepository @Database( - version = 14, + version = 15, entities = [ AttachmentEntity::class, LanguageEntity::class, @@ -66,6 +66,7 @@ import org.cru.godtools.db.room.repository.UserRoomRepository AutoMigration(from = 11, to = 12), AutoMigration(from = 12, to = 13), AutoMigration(from = 13, to = 14, spec = Migration14::class), + AutoMigration(from = 14, to = 15), ], ) @TypeConverters(Java8TimeConverters::class, LocaleConverter::class) @@ -119,6 +120,7 @@ internal abstract class GodToolsRoomDatabase : RoomDatabase() { * 12: 2023-06-08 * 13: 2023-09-18 * 14: 2023-09-18 + * 15: 2023-09-18 */ internal fun RoomDatabase.Builder.enableMigrations() = fallbackToDestructiveMigration() diff --git a/library/db/src/main/kotlin/org/cru/godtools/db/room/dao/ToolsDao.kt b/library/db/src/main/kotlin/org/cru/godtools/db/room/dao/ToolsDao.kt index 6cba3fa67e..4838290a9b 100644 --- a/library/db/src/main/kotlin/org/cru/godtools/db/room/dao/ToolsDao.kt +++ b/library/db/src/main/kotlin/org/cru/godtools/db/room/dao/ToolsDao.kt @@ -5,11 +5,13 @@ import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Update import androidx.room.Upsert import java.util.Locale import kotlinx.coroutines.flow.Flow import org.cru.godtools.db.room.entity.ToolEntity import org.cru.godtools.db.room.entity.partial.SyncTool +import org.cru.godtools.db.room.entity.partial.ToolFavorite import org.cru.godtools.model.Tool @Dao @@ -22,6 +24,8 @@ internal interface ToolsDao { fun findToolFlow(code: String): Flow @Query("SELECT * FROM tools WHERE id = :id") fun findToolByIdBlocking(id: Long): ToolEntity? + @Query("SELECT * FROM tools WHERE code = :code") + fun findToolFavorite(code: String): ToolFavorite? @Query("SELECT * FROM tools") suspend fun getResources(): List @@ -42,8 +46,8 @@ internal interface ToolsDao { fun insertOrIgnoreTools(tools: Collection) @Upsert(entity = ToolEntity::class) suspend fun upsertSyncTools(tools: Collection) - @Query("UPDATE tools SET isFavorite = :isFavorite WHERE code = :code") - suspend fun updateIsFavorite(code: String, isFavorite: Boolean) + @Update(entity = ToolEntity::class) + suspend fun update(tool: ToolFavorite) @Query("UPDATE tools SET `order` = ${Int.MAX_VALUE}") fun resetToolOrder() @Query("UPDATE tools SET `order` = :order WHERE code = :code") diff --git a/library/db/src/main/kotlin/org/cru/godtools/db/room/entity/ToolEntity.kt b/library/db/src/main/kotlin/org/cru/godtools/db/room/entity/ToolEntity.kt index 039729e74a..1d07401d61 100644 --- a/library/db/src/main/kotlin/org/cru/godtools/db/room/entity/ToolEntity.kt +++ b/library/db/src/main/kotlin/org/cru/godtools/db/room/entity/ToolEntity.kt @@ -36,6 +36,8 @@ internal class ToolEntity( val isHidden: Boolean = false, @ColumnInfo(defaultValue = "false") val isSpotlight: Boolean = false, + @ColumnInfo(defaultValue = "") + val changedFields: String = "", ) { constructor(tool: Tool) : this( id = tool.id, @@ -58,6 +60,7 @@ internal class ToolEntity( isFavorite = tool.isFavorite, isHidden = tool.isHidden, isSpotlight = tool.isSpotlight, + changedFields = tool.changedFieldsStr, ) fun toModel() = Tool().also { @@ -81,5 +84,6 @@ internal class ToolEntity( it.isFavorite = isFavorite it.isHidden = isHidden it.isSpotlight = isSpotlight + it.changedFieldsStr = changedFields } } diff --git a/library/db/src/main/kotlin/org/cru/godtools/db/room/entity/partial/ToolFavorite.kt b/library/db/src/main/kotlin/org/cru/godtools/db/room/entity/partial/ToolFavorite.kt new file mode 100644 index 0000000000..a5113d6a68 --- /dev/null +++ b/library/db/src/main/kotlin/org/cru/godtools/db/room/entity/partial/ToolFavorite.kt @@ -0,0 +1,21 @@ +package org.cru.godtools.db.room.entity.partial + +import androidx.room.ColumnInfo +import androidx.room.Ignore +import org.cru.godtools.model.ChangeTrackingModel +import org.cru.godtools.model.Tool + +internal class ToolFavorite(val code: String) : ChangeTrackingModel { + var isFavorite = false + set(value) { + if (field != value) markChanged(Tool.ATTR_IS_FAVORITE) + field = value + } + + // region ChangeTrackingModel + @Ignore + override var isTrackingChanges = false + @ColumnInfo(name = "changedFields") + override var changedFieldsStr = "" + // endregion ChangeTrackingModel +} diff --git a/library/db/src/main/kotlin/org/cru/godtools/db/room/repository/ToolsRoomRepository.kt b/library/db/src/main/kotlin/org/cru/godtools/db/room/repository/ToolsRoomRepository.kt index 585a3d75a2..89cd77a605 100644 --- a/library/db/src/main/kotlin/org/cru/godtools/db/room/repository/ToolsRoomRepository.kt +++ b/library/db/src/main/kotlin/org/cru/godtools/db/room/repository/ToolsRoomRepository.kt @@ -12,6 +12,7 @@ import org.cru.godtools.db.room.entity.ToolEntity import org.cru.godtools.db.room.entity.partial.SyncTool import org.cru.godtools.model.Resource import org.cru.godtools.model.Tool +import org.cru.godtools.model.trackChanges private val TOOL_TYPES = setOf(Tool.Type.TRACT, Tool.Type.CYOA, Tool.Type.ARTICLE) @@ -35,8 +36,19 @@ internal abstract class ToolsRoomRepository(private val db: GodToolsRoomDatabase override fun toolsChangeFlow(): Flow = db.changeFlow("tools") - override suspend fun pinTool(code: String) = dao.updateIsFavorite(code, true) - override suspend fun unpinTool(code: String) = dao.updateIsFavorite(code, false) + @Transaction + override suspend fun pinTool(code: String, trackChanges: Boolean) { + val tool = dao.findToolFavorite(code) ?: return + if (trackChanges) tool.isTrackingChanges = true + tool.isFavorite = true + dao.update(tool) + } + @Transaction + override suspend fun unpinTool(code: String) { + val tool = dao.findToolFavorite(code) ?: return + tool.trackChanges { it.isFavorite = false } + dao.update(tool) + } @Transaction override suspend fun storeToolOrder(tools: List) { diff --git a/library/db/src/test/kotlin/org/cru/godtools/db/repository/ToolsRepositoryIT.kt b/library/db/src/test/kotlin/org/cru/godtools/db/repository/ToolsRepositoryIT.kt index 2a6892eac8..e89d134569 100644 --- a/library/db/src/test/kotlin/org/cru/godtools/db/repository/ToolsRepositoryIT.kt +++ b/library/db/src/test/kotlin/org/cru/godtools/db/repository/ToolsRepositoryIT.kt @@ -287,31 +287,104 @@ abstract class ToolsRepositoryIT { } // endregion toolsChangeFlow() + // region pinTool() @Test - fun verifyPinTool() = testScope.runTest { + fun `pinTool()`() = testScope.runTest { val code = "pinTool" repository.storeInitialResources(listOf(Tool(code))) repository.findToolFlow(code).test { - assertFalse(assertNotNull(awaitItem()).isFavorite) + assertNotNull(awaitItem()) { + assertFalse(it.isFavorite) + assertFalse(Tool.ATTR_IS_FAVORITE in it.changedFields) + } + + repository.pinTool(code) + assertNotNull(awaitItem()) { + assertTrue(it.isFavorite) + assertTrue(Tool.ATTR_IS_FAVORITE in it.changedFields) + } + } + } + + @Test + fun `pinTool(trackChanges = false)`() = testScope.runTest { + val code = "pinTool" + repository.storeInitialResources(listOf(Tool(code))) + + repository.findToolFlow(code).test { + assertNotNull(awaitItem()) { + assertFalse(it.isFavorite) + assertFalse(Tool.ATTR_IS_FAVORITE in it.changedFields) + } + + repository.pinTool(code, trackChanges = false) + assertNotNull(awaitItem()) { + assertTrue(it.isFavorite) + assertFalse(Tool.ATTR_IS_FAVORITE in it.changedFields) + } + } + } + + @Test + fun `pinTool() - No Change`() = testScope.runTest { + val code = "pinTool" + repository.storeInitialResources(listOf(Tool(code) { isFavorite = true })) + + repository.findToolFlow(code).test { + assertNotNull(awaitItem()) { + assertTrue(it.isFavorite) + assertFalse(Tool.ATTR_IS_FAVORITE in it.changedFields) + } repository.pinTool(code) - assertTrue(assertNotNull(awaitItem()).isFavorite) + assertNotNull(awaitItem()) { + assertTrue(it.isFavorite) + assertFalse(Tool.ATTR_IS_FAVORITE in it.changedFields) + } } } + // endregion pinTool() + // region unpinTool() @Test - fun verifyUnpinTool() = testScope.runTest { + fun `unpinTool()`() = testScope.runTest { val code = "pinTool" repository.storeInitialResources(listOf(Tool(code) { isFavorite = true })) repository.findToolFlow(code).test { - assertTrue(assertNotNull(awaitItem()).isFavorite) + assertNotNull(awaitItem()) { + assertTrue(it.isFavorite) + assertFalse(Tool.ATTR_IS_FAVORITE in it.changedFields) + } repository.unpinTool(code) - assertFalse(assertNotNull(awaitItem()).isFavorite) + assertNotNull(awaitItem()) { + assertFalse(it.isFavorite) + assertTrue(Tool.ATTR_IS_FAVORITE in it.changedFields) + } + } + } + + @Test + fun `unpinTool() - No Change`() = testScope.runTest { + val code = "pinTool" + repository.storeInitialResources(listOf(Tool(code) { isFavorite = false })) + + repository.findToolFlow(code).test { + assertNotNull(awaitItem()) { + assertFalse(it.isFavorite) + assertFalse(Tool.ATTR_IS_FAVORITE in it.changedFields) + } + + repository.unpinTool(code) + assertNotNull(awaitItem()) { + assertFalse(it.isFavorite) + assertFalse(Tool.ATTR_IS_FAVORITE in it.changedFields) + } } } + // endregion unpinTool() // region storeToolOrder() @Test diff --git a/library/db/src/test/kotlin/org/cru/godtools/db/room/GodToolsRoomDatabaseMigrationIT.kt b/library/db/src/test/kotlin/org/cru/godtools/db/room/GodToolsRoomDatabaseMigrationIT.kt index 265def0c89..4476d21815 100644 --- a/library/db/src/test/kotlin/org/cru/godtools/db/room/GodToolsRoomDatabaseMigrationIT.kt +++ b/library/db/src/test/kotlin/org/cru/godtools/db/room/GodToolsRoomDatabaseMigrationIT.kt @@ -187,4 +187,23 @@ class GodToolsRoomDatabaseMigrationIT { } } } + + @Test + fun testMigrate14To15() { + // create v14 database + helper.createDatabase(GodToolsRoomDatabase.DATABASE_NAME, 14).use { db -> + db.execSQL("""INSERT INTO tools (id, code, type) VALUES (1, "a", "TRACT")""") + } + + // run migration + helper.runMigrationsAndValidate(GodToolsRoomDatabase.DATABASE_NAME, 15, true, *MIGRATIONS).use { db -> + db.query("SELECT id, code, changedFields FROM tools").use { + assertEquals(1, it.count) + it.moveToFirst() + assertEquals(1, it.getIntOrNull(0)) + assertEquals("a", it.getStringOrNull(1)) + assertEquals("", it.getStringOrNull(2)) + } + } + } } diff --git a/library/initial-content/src/main/kotlin/org/cru/godtools/init/content/task/Tasks.kt b/library/initial-content/src/main/kotlin/org/cru/godtools/init/content/task/Tasks.kt index 2238fc4b1e..851e169de2 100644 --- a/library/initial-content/src/main/kotlin/org/cru/godtools/init/content/task/Tasks.kt +++ b/library/initial-content/src/main/kotlin/org/cru/godtools/init/content/task/Tasks.kt @@ -88,7 +88,7 @@ internal class Tasks @Inject constructor( (preferred.await().asSequence().filter { available.contains(it) } + preferred.await().asSequence()) .distinct() .take(NUMBER_OF_FAVORITES) - .map { launch { toolsRepository.pinTool(it) } } + .map { launch { toolsRepository.pinTool(it, trackChanges = false) } } .toList().joinAll() } diff --git a/library/initial-content/src/test/kotlin/org/cru/godtools/init/content/task/TasksTest.kt b/library/initial-content/src/test/kotlin/org/cru/godtools/init/content/task/TasksTest.kt index c8154fc86a..092be1ed45 100644 --- a/library/initial-content/src/test/kotlin/org/cru/godtools/init/content/task/TasksTest.kt +++ b/library/initial-content/src/test/kotlin/org/cru/godtools/init/content/task/TasksTest.kt @@ -92,10 +92,10 @@ class TasksTest { tasks.initFavoriteTools() coVerifyAll { toolsRepository.getTools() - toolsRepository.pinTool("1") - toolsRepository.pinTool("2") - toolsRepository.pinTool("3") - toolsRepository.pinTool("5") + toolsRepository.pinTool("1", trackChanges = false) + toolsRepository.pinTool("2", trackChanges = false) + toolsRepository.pinTool("3", trackChanges = false) + toolsRepository.pinTool("5", trackChanges = false) } confirmVerified(toolsRepository) } diff --git a/library/model/src/main/kotlin/org/cru/godtools/model/ChangeTrackingModel.kt b/library/model/src/main/kotlin/org/cru/godtools/model/ChangeTrackingModel.kt new file mode 100644 index 0000000000..14b9a5eda3 --- /dev/null +++ b/library/model/src/main/kotlin/org/cru/godtools/model/ChangeTrackingModel.kt @@ -0,0 +1,21 @@ +package org.cru.godtools.model + +interface ChangeTrackingModel { + val changedFields get() = changedFieldsStr.splitToSequence(",").filter { it.isNotEmpty() }.distinct() + + var isTrackingChanges: Boolean + var changedFieldsStr: String + + fun markChanged(field: String) { + if (isTrackingChanges) changedFieldsStr = "$changedFieldsStr,$field" + } +} + +inline fun T.trackChanges(block: (T) -> Unit) { + isTrackingChanges = true + try { + block(this) + } finally { + isTrackingChanges = false + } +} diff --git a/library/model/src/main/kotlin/org/cru/godtools/model/Tool.kt b/library/model/src/main/kotlin/org/cru/godtools/model/Tool.kt index 9e72da268e..df9e7e62db 100644 --- a/library/model/src/main/kotlin/org/cru/godtools/model/Tool.kt +++ b/library/model/src/main/kotlin/org/cru/godtools/model/Tool.kt @@ -32,13 +32,15 @@ private const val JSON_INITIAL_FAVORITES_PRIORITY = "attr-initial-favorites-prio private const val JSON_SCREEN_SHARE_DISABLED = "attr-screen-share-disabled" @JsonApiType(JSON_API_TYPE) -class Tool : Base() { +class Tool : Base(), ChangeTrackingModel { companion object { const val JSON_ATTACHMENTS = "attachments" const val JSON_LATEST_TRANSLATIONS = "latest-translations" const val JSON_METATOOL = "metatool" const val JSON_DEFAULT_VARIANT = "default-variant" + const val ATTR_IS_FAVORITE = "isFavorite" + val COMPARATOR_DEFAULT_ORDER = compareBy { it.defaultOrder } val COMPARATOR_FAVORITE_ORDER = compareBy { it.order }.then(COMPARATOR_DEFAULT_ORDER) } @@ -144,6 +146,13 @@ class Tool : Base() { var isSpotlight = false val isValid get() = code != null && id != INVALID_ID + + // region ChangeTrackingModel + @JsonApiIgnore + override var changedFieldsStr = "" + @JsonApiIgnore + override var isTrackingChanges = false + // endregion Change Tracking } // TODO: move this to testFixtures once they support Kotlin source files