Skip to content

Commit

Permalink
Merge pull request #3110 from CruGlobal/trackChangedFields
Browse files Browse the repository at this point in the history
GT-1796 Track which tools were added/removed as favorites
  • Loading branch information
frett authored Sep 20, 2023
2 parents dcbd673 + 3085d96 commit 81137ef
Show file tree
Hide file tree
Showing 13 changed files with 851 additions and 18 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface ToolsRepository {

fun toolsChangeFlow(): Flow<Any?>

suspend fun pinTool(code: String)
suspend fun pinTool(code: String, trackChanges: Boolean = true)
suspend fun unpinTool(code: String)

suspend fun storeToolOrder(tools: List<String>)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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<GodToolsRoomDatabase>.enableMigrations() = fallbackToDestructiveMigration()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,6 +24,8 @@ internal interface ToolsDao {
fun findToolFlow(code: String): Flow<ToolEntity?>
@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<ToolEntity>
Expand All @@ -42,8 +46,8 @@ internal interface ToolsDao {
fun insertOrIgnoreTools(tools: Collection<ToolEntity>)
@Upsert(entity = ToolEntity::class)
suspend fun upsertSyncTools(tools: Collection<SyncTool>)
@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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -58,6 +60,7 @@ internal class ToolEntity(
isFavorite = tool.isFavorite,
isHidden = tool.isHidden,
isSpotlight = tool.isSpotlight,
changedFields = tool.changedFieldsStr,
)

fun toModel() = Tool().also {
Expand All @@ -81,5 +84,6 @@ internal class ToolEntity(
it.isFavorite = isFavorite
it.isHidden = isHidden
it.isSpotlight = isSpotlight
it.changedFieldsStr = changedFields
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -35,8 +36,19 @@ internal abstract class ToolsRoomRepository(private val db: GodToolsRoomDatabase

override fun toolsChangeFlow(): Flow<Any?> = 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<String>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 : ChangeTrackingModel> T.trackChanges(block: (T) -> Unit) {
isTrackingChanges = true
try {
block(this)
} finally {
isTrackingChanges = false
}
}
11 changes: 10 additions & 1 deletion library/model/src/main/kotlin/org/cru/godtools/model/Tool.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Tool> { it.defaultOrder }
val COMPARATOR_FAVORITE_ORDER = compareBy<Tool> { it.order }.then(COMPARATOR_DEFAULT_ORDER)
}
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 81137ef

Please sign in to comment.