Skip to content

Commit 232961a

Browse files
authored
Release Student 8.4.2 (285)
2 parents 1d8078e + 6ee9546 commit 232961a

File tree

7 files changed

+153
-17
lines changed

7 files changed

+153
-17
lines changed

apps/student/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ android {
3838
applicationId "com.instructure.candroid"
3939
minSdkVersion Versions.MIN_SDK
4040
targetSdkVersion Versions.TARGET_SDK
41-
versionCode = 284
42-
versionName = '8.4.1'
41+
versionCode = 285
42+
versionName = '8.4.2'
4343

4444
vectorDrawables.useSupportLibrary = true
4545
testInstrumentationRunner 'com.instructure.student.espresso.StudentHiltTestRunner'

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 }

apps/student/src/test/java/com/instructure/student/widget/todo/ToDoWidgetUpdaterTest.kt

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ class ToDoWidgetUpdaterTest {
124124
fun `Emits Content when api returns data and maps correctly`() = runTest {
125125
val assignmentItem = createPlannerItem(
126126
plannableType = PlannableType.ASSIGNMENT,
127+
plannableId = 1,
127128
date = createDate(2024, 1, 5, 2),
128129
courseId = 1
129130
)
@@ -139,6 +140,7 @@ class ToDoWidgetUpdaterTest {
139140

140141
val calendarEvent = createPlannerItem(
141142
plannableType = PlannableType.CALENDAR_EVENT,
143+
plannableId = 3,
142144
date = createDate(2025, 5, 21, 12),
143145
userId = 1,
144146
startAt = createDate(2025, 5, 21, 12),
@@ -176,9 +178,9 @@ class ToDoWidgetUpdaterTest {
176178
R.drawable.ic_calendar,
177179
apiPrefs.user.color,
178180
"Context Name",
179-
"Plannable 1",
181+
"Plannable 3",
180182
"All day",
181-
"/users/1/calendar_events/1"
183+
"/users/1/calendar_events/3"
182184
)
183185
)
184186
)
@@ -190,20 +192,23 @@ class ToDoWidgetUpdaterTest {
190192
fun `Shows only incomplete items`() = runTest {
191193
val submittedAssignmentItem = createPlannerItem(
192194
plannableType = PlannableType.ASSIGNMENT,
195+
plannableId = 1,
193196
date = createDate(2024, 1, 5, 2),
194197
courseId = 1,
195198
submitted = true
196199
)
197200

198201
val submittedDiscussionItem = createPlannerItem(
199202
plannableType = PlannableType.DISCUSSION_TOPIC,
203+
plannableId = 2,
200204
date = createDate(2024, 1, 5, 2),
201205
courseId = 1,
202206
submitted = true
203207
)
204208

205209
val submittedSubAssignmentItem = createPlannerItem(
206210
plannableType = PlannableType.SUB_ASSIGNMENT,
211+
plannableId = 3,
207212
date = createDate(2024, 1, 5, 2),
208213
courseId = 1,
209214
submitted = true
@@ -212,7 +217,7 @@ class ToDoWidgetUpdaterTest {
212217
val completedToDoItem = createPlannerItem(
213218
plannableType = PlannableType.PLANNER_NOTE,
214219
date = createDate(2023, 10, 1, 12),
215-
plannableId = 2,
220+
plannableId = 4,
216221
userId = 1,
217222
startAt = createDate(2023, 10, 1, 12),
218223
endAt = createDate(2023, 10, 1, 13),
@@ -221,22 +226,24 @@ class ToDoWidgetUpdaterTest {
221226

222227
val subAssignmentItem = createPlannerItem(
223228
plannableType = PlannableType.SUB_ASSIGNMENT,
229+
plannableId = 5,
224230
date = createDate(2024, 1, 5, 2),
225231
courseId = 1,
226232
submitted = false
227233
)
228234

229235
val toDoItem = createPlannerItem(
230236
plannableType = PlannableType.PLANNER_NOTE,
237+
plannableId = 6,
231238
date = createDate(2023, 10, 1, 12),
232-
plannableId = 2,
233239
userId = 1,
234240
startAt = createDate(2023, 10, 1, 12),
235241
endAt = createDate(2023, 10, 1, 13)
236242
)
237243

238244
val calendarEvent = createPlannerItem(
239245
plannableType = PlannableType.CALENDAR_EVENT,
246+
plannableId = 7,
240247
date = createDate(2025, 5, 21, 12),
241248
userId = 1,
242249
startAt = createDate(2025, 5, 21, 12),
@@ -264,16 +271,16 @@ class ToDoWidgetUpdaterTest {
264271
R.drawable.ic_todo,
265272
apiPrefs.user.color,
266273
"To Do",
267-
"Plannable 2",
274+
"Plannable 6",
268275
"12:00",
269-
"/todos/2"
276+
"/todos/6"
270277
),
271278
WidgetPlannerItem(
272279
LocalDate.of(2024, 1, 5),
273280
R.drawable.ic_discussion,
274281
subAssignmentItem.canvasContext.color,
275282
"CODE",
276-
"Plannable 1",
283+
"Plannable 5",
277284
"02:00",
278285
"https://htmlurl.com"
279286
),
@@ -282,9 +289,9 @@ class ToDoWidgetUpdaterTest {
282289
R.drawable.ic_calendar,
283290
apiPrefs.user.color,
284291
"Context Name",
285-
"Plannable 1",
292+
"Plannable 7",
286293
"All day",
287-
"/users/1/calendar_events/1"
294+
"/users/1/calendar_events/7"
288295
)
289296
)
290297
)

libs/canvas-api-2/build.gradle

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ apply plugin: 'kotlin-parcelize'
2222
apply plugin: 'com.google.devtools.ksp'
2323
apply plugin: 'dagger.hilt.android.plugin'
2424

25-
def pineDebugBaseUrl = "https://pine-api-dev.domain-svcs.nonprod.inseng.io"
26-
def pineReleaseBaseUrl = "https://pine-api-production.us-east-1.temp.prod.inseng.io"
25+
def pineDebugBaseUrl = "https://pine-api-dev.us-east-1.core.inseng.io"
26+
def pineReleaseBaseUrl = "https://pine-api.us-east-1.core.inseng.io"
2727

28-
def cedarDebugBaseUrl = "https://cedar-api-dev.domain-svcs.nonprod.inseng.io"
29-
def cedarReleaseBaseUrl = "https://cedar-api-production.us-east-1.temp.prod.inseng.io"
28+
def cedarDebugBaseUrl = "https://cedar-api-dev.us-east-1.core.inseng.io"
29+
def cedarReleaseBaseUrl = "https://cedar-api.us-east-1.core.inseng.io"
3030

31-
def redwoodDebugBaseUrl = "https://redwood-api-dev.domain-svcs.nonprod.inseng.io"
32-
def redwoodReleaseBaseUrl = "https://redwood-api-production.us-east-1.temp.prod.inseng.io"
31+
def redwoodDebugBaseUrl = "https://redwood-api-dev.us-east-1.core.inseng.io"
32+
def redwoodReleaseBaseUrl = "https://redwood-api.us-east-1.core.inseng.io"
3333

3434
def journeyDebugBaseUrl = "https://journey-server-dev.journey.nonprod.inseng.io"
3535
def journeyReleaseBaseUrl = "https://journey-server-prod.us-east-1.temp.prod.inseng.io"

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)