Skip to content

Commit b21a76a

Browse files
authored
Fixed duplicate key issue (#3452)
* Fixed duplicate key issue * Distinct by id * Add distinctBy to other places.
1 parent d5f7555 commit b21a76a

File tree

4 files changed

+129
-0
lines changed

4 files changed

+129
-0
lines changed

apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ abstract class CallbackActivity : ParentActivity(), OnUnreadCountInvalidated, No
266266

267267
// Filter planner items - exclude announcements, assessment requests, completed items
268268
val todoCount = plannerItems.dataOrNull
269+
?.distinctBy { it.id }
269270
?.filter { it.plannableType != PlannableType.ANNOUNCEMENT && it.plannableType != PlannableType.ASSESSMENT_REQUEST && !it.isComplete() }
270271
?.filterByToDoFilters(todoFilters, filteredCourses)
271272
?.count() ?: 0

apps/student/src/main/java/com/instructure/student/widget/todo/ToDoWidgetUpdater.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class ToDoWidgetUpdater(
6969
}
7070
// Other errors are handled in catch
7171
val plannerItems = plannerItemsDataResult.dataOrThrow
72+
.distinctBy { it.id }
7273
.filterByToDoFilters(todoFilters, courses)
7374
.filter { !it.isComplete() }
7475
.sortedBy { it.comparisonDate }

libs/pandautils/src/main/java/com/instructure/pandautils/features/todolist/ToDoListViewModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ class ToDoListViewModel @Inject constructor(
150150
val courses = repository.getCourses(forceRefresh).dataOrThrow
151151
val plannerItems = repository.getPlannerItems(startDate, endDate, forceRefresh).dataOrThrow
152152
.filter { it.plannableType != PlannableType.ANNOUNCEMENT && it.plannableType != PlannableType.ASSESSMENT_REQUEST }
153+
.distinctBy { it.id }
153154

154155
// Store planner items for later reference
155156
plannerItemsMap.clear()

libs/pandautils/src/test/java/com/instructure/pandautils/features/todolist/ToDoListViewModelTest.kt

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1562,6 +1562,132 @@ class ToDoListViewModelTest {
15621562
assertEquals(0, viewModel.uiState.value.toDoCount)
15631563
}
15641564

1565+
@Test
1566+
fun `ViewModel handles duplicate planner items with same ID`() = runTest {
1567+
val date1 = Date(1704067200000L) // Jan 1, 2024
1568+
val date2 = Date(1704153600000L) // Jan 2, 2024
1569+
1570+
// Create duplicate planner items with the same ID but different dates
1571+
val plannerItems = listOf(
1572+
createPlannerItem(id = 1L, title = "Assignment 1", plannableDate = date1),
1573+
createPlannerItem(id = 1L, title = "Assignment 1", plannableDate = date2), // Duplicate ID!
1574+
createPlannerItem(id = 2L, title = "Assignment 2", plannableDate = date1)
1575+
)
1576+
1577+
coEvery { repository.getCourses(any()) } returns DataResult.Success(emptyList())
1578+
coEvery { repository.getPlannerItems(any(), any(), any()) } returns DataResult.Success(plannerItems)
1579+
1580+
val viewModel = getViewModel()
1581+
1582+
val uiState = viewModel.uiState.value
1583+
val allItems = uiState.itemsByDate.values.flatten()
1584+
1585+
// Should only have 2 unique items (duplicates removed)
1586+
assertEquals(2, allItems.size)
1587+
1588+
// Verify both unique IDs are present
1589+
assertTrue(allItems.any { it.id == "1" })
1590+
assertTrue(allItems.any { it.id == "2" })
1591+
1592+
// Verify no duplicate IDs exist
1593+
val itemIds = allItems.map { it.id }
1594+
assertEquals(itemIds.size, itemIds.distinct().size)
1595+
}
1596+
1597+
@Test
1598+
fun `ViewModel handles multiple duplicate planner items`() = runTest {
1599+
val date = Date(1704067200000L)
1600+
1601+
// Create multiple duplicates
1602+
val plannerItems = listOf(
1603+
createPlannerItem(id = 1L, title = "Assignment 1", plannableDate = date),
1604+
createPlannerItem(id = 1L, title = "Assignment 1", plannableDate = date), // Duplicate
1605+
createPlannerItem(id = 1L, title = "Assignment 1", plannableDate = date), // Duplicate
1606+
createPlannerItem(id = 2L, title = "Assignment 2", plannableDate = date),
1607+
createPlannerItem(id = 2L, title = "Assignment 2", plannableDate = date), // Duplicate
1608+
createPlannerItem(id = 3L, title = "Assignment 3", plannableDate = date)
1609+
)
1610+
1611+
coEvery { repository.getCourses(any()) } returns DataResult.Success(emptyList())
1612+
coEvery { repository.getPlannerItems(any(), any(), any()) } returns DataResult.Success(plannerItems)
1613+
1614+
val viewModel = getViewModel()
1615+
1616+
val uiState = viewModel.uiState.value
1617+
val allItems = uiState.itemsByDate.values.flatten()
1618+
1619+
// Should only have 3 unique items
1620+
assertEquals(3, allItems.size)
1621+
1622+
// Verify all unique IDs are present
1623+
assertTrue(allItems.any { it.id == "1" })
1624+
assertTrue(allItems.any { it.id == "2" })
1625+
assertTrue(allItems.any { it.id == "3" })
1626+
1627+
// Verify no duplicate IDs exist
1628+
val itemIds = allItems.map { it.id }
1629+
assertEquals(itemIds.size, itemIds.distinct().size)
1630+
}
1631+
1632+
@Test
1633+
fun `ViewModel handles duplicates across different date groups`() = runTest {
1634+
val date1 = Date(1704067200000L) // Jan 1, 2024
1635+
val date2 = Date(1704153600000L) // Jan 2, 2024
1636+
1637+
// Same assignment appearing on two different dates (backend anomaly)
1638+
val plannerItems = listOf(
1639+
createPlannerItem(id = 1L, title = "Assignment 1", plannableDate = date1),
1640+
createPlannerItem(id = 1L, title = "Assignment 1", plannableDate = date2), // Same ID, different date
1641+
createPlannerItem(id = 2L, title = "Assignment 2", plannableDate = date1)
1642+
)
1643+
1644+
coEvery { repository.getCourses(any()) } returns DataResult.Success(emptyList())
1645+
coEvery { repository.getPlannerItems(any(), any(), any()) } returns DataResult.Success(plannerItems)
1646+
1647+
val viewModel = getViewModel()
1648+
1649+
val uiState = viewModel.uiState.value
1650+
1651+
// Should have 2 unique items total (first occurrence of each ID kept)
1652+
val allItems = uiState.itemsByDate.values.flatten()
1653+
assertEquals(2, allItems.size)
1654+
1655+
// Verify IDs are unique
1656+
val itemIds = allItems.map { it.id }
1657+
assertEquals(itemIds.size, itemIds.distinct().size)
1658+
1659+
// Should still have items grouped by date, but no duplicate IDs
1660+
assertTrue(uiState.itemsByDate.keys.size <= 2)
1661+
}
1662+
1663+
@Test
1664+
fun `ViewModel preserves first occurrence when duplicates exist`() = runTest {
1665+
val date1 = Date(1704067200000L) // Jan 1, 2024
1666+
val date2 = Date(1704153600000L) // Jan 2, 2024
1667+
1668+
// First occurrence should be kept (Jan 1), second occurrence should be filtered out (Jan 2)
1669+
val plannerItems = listOf(
1670+
createPlannerItem(id = 1L, title = "Assignment 1", courseId = 100L, plannableDate = date1),
1671+
createPlannerItem(id = 1L, title = "Assignment 1 Duplicate", courseId = 200L, plannableDate = date2)
1672+
)
1673+
1674+
coEvery { repository.getCourses(any()) } returns DataResult.Success(emptyList())
1675+
coEvery { repository.getPlannerItems(any(), any(), any()) } returns DataResult.Success(plannerItems)
1676+
1677+
val viewModel = getViewModel()
1678+
1679+
val uiState = viewModel.uiState.value
1680+
val allItems = uiState.itemsByDate.values.flatten()
1681+
1682+
// Should only have 1 item
1683+
assertEquals(1, allItems.size)
1684+
1685+
// First item should be kept (the one with date1)
1686+
val item = allItems.first()
1687+
assertEquals("1", item.id)
1688+
assertEquals(date1, item.date)
1689+
}
1690+
15651691
// Helper functions
15661692
private fun getViewModel(): ToDoListViewModel {
15671693
return ToDoListViewModel(context, repository, networkStateProvider, firebaseCrashlytics, toDoFilterDao, apiPrefs, analytics, toDoListViewModelBehavior, calendarSharedEvents)

0 commit comments

Comments
 (0)