Skip to content

Commit ac89807

Browse files
authored
Calendar Filter crash (#2938)
refs: MBL-17976 affects: Student, Teacher, Parent release note: none
1 parent 42959c9 commit ac89807

File tree

13 files changed

+216
-95
lines changed

13 files changed

+216
-95
lines changed

Core/Core/API/ID.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ public struct ID: Codable, Equatable, Hashable, CustomStringConvertible, RawRepr
4040
return
4141
}
4242

43+
Analytics.shared.logError(
44+
name: "Empty ID decoded from unhandled data",
45+
reason: "baseUrl: \(Analytics.analyticsBaseUrl)"
46+
)
47+
4348
value = ""
4449
}
4550

Core/Core/Analytics/Analytics.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,11 @@ public class Analytics: NSObject {
113113

114114
return name
115115
}
116+
117+
public static var analyticsBaseUrl: String {
118+
guard let session = AppEnvironment.shared.currentSession else {
119+
return ""
120+
}
121+
return session.baseURL.absoluteString
122+
}
116123
}

Core/Core/Contexts/Context.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ public struct Context: Codable, Equatable, Hashable {
3737
public var pathComponent: String { "\(contextType.pathComponent)/\(id)" }
3838

3939
public init(_ contextType: ContextType, id: String) {
40+
if id.isEmpty {
41+
Analytics.shared.logError(
42+
name: "Context created with invalid contextId",
43+
reason: "contextType: \(contextType.rawValue), contextId: \"\(id)\", baseUrl: \(Analytics.analyticsBaseUrl)"
44+
)
45+
}
46+
4047
self.contextType = contextType
4148
self.id = ID.expandTildeID(id)
4249
}
@@ -77,4 +84,8 @@ public extension Context {
7784
var courseId: String? { contextType == .course ? id : nil }
7885
var groupId: String? { contextType == .group ? id : nil }
7986
var userId: String? { contextType == .user ? id : nil }
87+
88+
var isValid: Bool {
89+
id.isNotEmpty
90+
}
8091
}

Core/Core/Planner/CalendarFilter/Model/CoreData/CDCalendarFilterEntry.swift

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public class CDCalendarFilterEntry: NSManagedObject {
3737
@NSManaged public var name: String
3838
/// For the observer role we have a separate list of filters for each observed student
3939
@NSManaged public var observedUserId: String?
40-
@NSManaged public private(set) var rawContextID: String
40+
@NSManaged public private(set) var rawContextID: String // example: "course_42"
4141
@NSManaged public var rawPurpose: Int16
4242

4343
public var context: Context {
@@ -49,6 +49,10 @@ public class CDCalendarFilterEntry: NSManagedObject {
4949
}
5050
}
5151

52+
public var wrappedContext: Context? {
53+
Context(canvasContextID: rawContextID)
54+
}
55+
5256
public var purpose: CDCalendarFilterPurpose {
5357
get {
5458
CDCalendarFilterPurpose(rawValue: rawPurpose) ?? .unknown
@@ -80,6 +84,81 @@ public class CDCalendarFilterEntry: NSManagedObject {
8084
public var courseName: String? {
8185
context.contextType == .course ? name : nil
8286
}
87+
88+
@discardableResult
89+
public static func save(
90+
context: Context,
91+
observedUserId: String? = nil,
92+
name: String,
93+
purpose: CDCalendarFilterPurpose = .unknown,
94+
in moContext: NSManagedObjectContext
95+
) -> CDCalendarFilterEntry? {
96+
guard context.isValid else {
97+
Analytics.shared.logError(
98+
name: "CDCalendarFilterEntry save failed with invalid contextId",
99+
reason: "contextType: \(context.contextType.rawValue), contextId: \"\(context.id)\", baseUrl: \(Analytics.analyticsBaseUrl)"
100+
)
101+
return nil
102+
}
103+
104+
let canvasContextID = context.canvasContextID
105+
106+
let predicate = NSPredicate(key: (\CDCalendarFilterEntry.rawContextID).string, equals: canvasContextID)
107+
.and(NSPredicate(key: (\CDCalendarFilterEntry.observedUserId).string, equals: observedUserId))
108+
109+
let model: CDCalendarFilterEntry = moContext.fetch(predicate).first ?? moContext.insert()
110+
model.rawContextID = canvasContextID
111+
model.observedUserId = observedUserId
112+
model.name = name
113+
model.purpose = purpose
114+
return model
115+
}
116+
117+
@discardableResult
118+
public static func save(
119+
userId: String,
120+
userName: String,
121+
courses: [APICourse],
122+
groups: [APIGroup],
123+
observedUserId: String? = nil,
124+
purpose: CDCalendarFilterPurpose = .unknown,
125+
in moContext: NSManagedObjectContext
126+
) -> [CDCalendarFilterEntry] {
127+
// save user filter
128+
let userFilters = [
129+
CDCalendarFilterEntry.save(
130+
context: .user(userId),
131+
observedUserId: observedUserId,
132+
name: userName,
133+
purpose: purpose,
134+
in: moContext
135+
)
136+
].compactMap { $0 }
137+
138+
// save course filters
139+
let courseFilters = courses.compactMap { course in
140+
CDCalendarFilterEntry.save(
141+
context: .course(course.id.value),
142+
observedUserId: observedUserId,
143+
name: course.name ?? "",
144+
purpose: purpose,
145+
in: moContext
146+
)
147+
}
148+
149+
// save group filters
150+
let groupFilters = groups.compactMap { group in
151+
CDCalendarFilterEntry.save(
152+
context: .group(group.id.value),
153+
observedUserId: observedUserId,
154+
name: group.name,
155+
purpose: purpose,
156+
in: moContext
157+
)
158+
}
159+
160+
return userFilters + courseFilters + groupFilters
161+
}
83162
}
84163

85164
extension CDCalendarFilterEntry: Comparable {

Core/Core/Planner/CalendarFilter/Model/Interactor/CalendarFilterInteractor.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,15 +163,17 @@ public class CalendarFilterInteractorLive: CalendarFilterInteractor {
163163
private extension Set where Element == Context {
164164

165165
func removeUnavailableFilters(filters: [CDCalendarFilterEntry]) -> Set<Element> {
166-
let availableContexts = filters.map { $0.context }
166+
// using compactMap here to handle invalid contextIDs, which may have been cached before
167+
let availableContexts = filters.compactMap { $0.wrappedContext }
167168
return intersection(availableContexts)
168169
}
169170
}
170171

171172
private extension Array where Element == CDCalendarFilterEntry {
172173

173174
func defaultFilters(limit: CalendarFilterCountLimit) -> Set<Context> {
174-
let contexts = sorted().map { $0.context }
175+
// using compactMap here to handle invalid contextIDs, which may have been cached before
176+
let contexts = sorted().compactMap { $0.wrappedContext }
175177
return Set(contexts.prefix(limit.rawValue))
176178
}
177179
}

Core/Core/Planner/CalendarFilter/Model/Interactor/FilterProviders/CalendarFilterEntryProviderStudent.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ struct CalendarFilterEntryProviderStudent: CalendarFilterEntryProvider {
3535
return nil
3636
}
3737

38-
let useCase = GetCalendarFilters(currentUserName: userName,
39-
currentUserId: userId,
40-
states: [.current_and_concluded],
41-
filterUnpublishedCourses: true)
38+
let useCase = GetStudentCalendarFilters(
39+
currentUserName: userName,
40+
currentUserId: userId,
41+
states: [.current_and_concluded],
42+
filterUnpublishedCourses: true
43+
)
4244
return ReactiveStore(useCase: useCase).getEntities(ignoreCache: ignoreCache)
4345
}
4446
}

Core/Core/Planner/CalendarFilter/Model/UseCase/GetParentCalendarFilters.swift

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -86,35 +86,17 @@ class GetParentCalendarFilters: UseCase {
8686
.store(in: &subscriptions)
8787
}
8888

89-
func write(
90-
response: APIResponse?,
91-
urlResponse: URLResponse?,
92-
to client: NSManagedObjectContext
93-
) {
94-
guard let courses = response?.courses,
95-
let groups = response?.groups
96-
else {
97-
return
98-
}
99-
100-
let filter: CDCalendarFilterEntry = client.insert()
101-
filter.context = .user(userId)
102-
filter.name = userName
103-
filter.observedUserId = observedUserId
89+
func write(response: APIResponse?, urlResponse: URLResponse?, to client: NSManagedObjectContext) {
90+
guard let response else { return }
10491

105-
courses.forEach { course in
106-
let filter: CDCalendarFilterEntry = client.insert()
107-
filter.context = .course(course.id.rawValue)
108-
filter.name = course.name ?? ""
109-
filter.observedUserId = observedUserId
110-
}
111-
112-
groups.forEach { group in
113-
let filter: CDCalendarFilterEntry = client.insert()
114-
filter.context = .group(group.id.rawValue)
115-
filter.name = group.name
116-
filter.observedUserId = observedUserId
117-
}
92+
CDCalendarFilterEntry.save(
93+
userId: userId,
94+
userName: userName,
95+
courses: response.courses,
96+
groups: response.groups,
97+
observedUserId: observedUserId,
98+
in: client
99+
)
118100
}
119101

120102
func reset(context: NSManagedObjectContext) {

Core/Core/Planner/CalendarFilter/Model/UseCase/GetCalendarFilters.swift renamed to Core/Core/Planner/CalendarFilter/Model/UseCase/GetStudentCalendarFilters.swift

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import CoreData
2020
import Combine
2121
import SwiftUI
2222

23-
class GetCalendarFilters: UseCase {
23+
class GetStudentCalendarFilters: UseCase {
2424
struct APIResponse: Codable {
2525
let courses: [APICourse]
2626
let groups: [APIGroup]
@@ -103,32 +103,16 @@ class GetCalendarFilters: UseCase {
103103
.store(in: &subscriptions)
104104
}
105105

106-
func write(
107-
response: APIResponse?,
108-
urlResponse: URLResponse?,
109-
to client: NSManagedObjectContext
110-
) {
111-
guard let courses = response?.courses,
112-
let groups = response?.groups
113-
else {
114-
return
115-
}
106+
func write(response: APIResponse?, urlResponse: URLResponse?, to client: NSManagedObjectContext) {
107+
guard let response else { return }
116108

117-
let filter: CDCalendarFilterEntry = client.insert()
118-
filter.context = .user(userId)
119-
filter.name = userName
120-
121-
courses.forEach { course in
122-
let filter: CDCalendarFilterEntry = client.insert()
123-
filter.context = .course(course.id.rawValue)
124-
filter.name = course.name ?? ""
125-
}
126-
127-
groups.forEach { group in
128-
let filter: CDCalendarFilterEntry = client.insert()
129-
filter.context = .group(group.id.rawValue)
130-
filter.name = group.name
131-
}
109+
CDCalendarFilterEntry.save(
110+
userId: userId,
111+
userName: userName,
112+
courses: response.courses,
113+
groups: response.groups,
114+
in: client
115+
)
132116
}
133117

134118
func reset(context: NSManagedObjectContext) {

Core/Core/Planner/CalendarFilter/Model/UseCase/GetTeacherCalendarFilters.swift

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -103,35 +103,17 @@ class GetTeacherCalendarFilters: UseCase {
103103
.store(in: &subscriptions)
104104
}
105105

106-
func write(
107-
response: APIResponse?,
108-
urlResponse: URLResponse?,
109-
to client: NSManagedObjectContext
110-
) {
111-
guard let courses = response?.courses,
112-
let groups = response?.groups
113-
else {
114-
return
115-
}
116-
117-
let filter: CDCalendarFilterEntry = client.insert()
118-
filter.context = .user(userId)
119-
filter.name = userName
120-
filter.purpose = purpose.filterPurpose
121-
122-
courses.forEach { course in
123-
let filter: CDCalendarFilterEntry = client.insert()
124-
filter.context = .course(course.id.rawValue)
125-
filter.name = course.name ?? ""
126-
filter.purpose = purpose.filterPurpose
127-
}
128-
129-
groups.forEach { group in
130-
let filter: CDCalendarFilterEntry = client.insert()
131-
filter.context = .group(group.id.rawValue)
132-
filter.name = group.name
133-
filter.purpose = purpose.filterPurpose
134-
}
106+
func write(response: APIResponse?, urlResponse: URLResponse?, to client: NSManagedObjectContext) {
107+
guard let response else { return }
108+
109+
CDCalendarFilterEntry.save(
110+
userId: userId,
111+
userName: userName,
112+
courses: response.courses,
113+
groups: response.groups,
114+
purpose: purpose.filterPurpose,
115+
in: client
116+
)
135117
}
136118

137119
func reset(context: NSManagedObjectContext) {

Core/CoreTests/Analytics/AnalyticsTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,12 @@ class AnalyticsTests: XCTestCase {
9797
AppEnvironment.shared.app = .teacher
9898
XCTAssertEqual(Analytics.analyticsAppName, "teacher")
9999
}
100+
101+
func testAnalyticsBaseUrl() {
102+
AppEnvironment.shared.currentSession = nil
103+
XCTAssertEqual(Analytics.analyticsBaseUrl, "")
104+
105+
AppEnvironment.shared.currentSession = .make(baseURL: URL(string: "https://canvas.instructure.com")!)
106+
XCTAssertEqual(Analytics.analyticsBaseUrl, "https://canvas.instructure.com")
107+
}
100108
}

0 commit comments

Comments
 (0)