Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issue of user not being able to submit assignment when logged in root account #2976

Closed
wants to merge 9 commits into from
4 changes: 4 additions & 0 deletions Core/Core/Contexts/Tab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ public class Tab: NSManagedObject {
set { visibilityRaw = newValue.rawValue }
}

public var apiInstanceHost: String? {
return fullURL?.host()
}

public var name: TabName {
TabName(rawValue: id) ?? .custom
}
Expand Down
99 changes: 78 additions & 21 deletions Core/Core/Submissions/GetSubmissions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,39 @@ public struct GetRecentlyGradedSubmissions: CollectionUseCase {
}
}

public struct SubmissionDestination {
public let courseID: String
public let assignmentID: String
public let userID: String
public let apiInstanceHost: String?
public var context: Context { .course(courseID) }

public init(courseID: String, assignmentID: String, userID: String, apiInstanceHost: String? = nil) {
self.userID = userID
self.courseID = courseID
self.assignmentID = assignmentID
self.apiInstanceHost = apiInstanceHost
}

func baseURL(in env: AppEnvironment) -> URL? {
guard let host = apiInstanceHost else { return nil }

var urlComps = URLComponents()
urlComps.host = host
urlComps.scheme = env.api.baseURL.scheme
return urlComps.url
}
}

public class CreateSubmission: APIUseCase {
let context: Context
let assignmentID: String
let userID: String
let destination: SubmissionDestination
var apiCoordinator: SubmissionApiCoordinator
vargaat marked this conversation as resolved.
Show resolved Hide resolved

public let request: CreateSubmissionRequest
public typealias Model = Submission

public init(
context: Context,
assignmentID: String,
userID: String,
destination: SubmissionDestination,
submissionType: SubmissionType,
textComment: String? = nil,
isGroupComment: Bool? = nil,
Expand All @@ -59,9 +81,8 @@ public class CreateSubmission: APIUseCase {
mediaCommentType: MediaCommentType? = nil,
annotatableAttachmentID: String? = nil
) {
self.context = context
self.assignmentID = assignmentID
self.userID = userID
self.destination = destination
self.apiCoordinator = DefaultSubmissionApiCoordinator(destination: destination)

let submission = CreateSubmissionRequest.Body.Submission(
annotatable_attachment_id: annotatableAttachmentID,
Expand All @@ -76,8 +97,8 @@ public class CreateSubmission: APIUseCase {
)

request = CreateSubmissionRequest(
context: context,
assignmentID: assignmentID,
context: destination.context,
assignmentID: destination.assignmentID,
body: .init(submission: submission)
)
}
Expand All @@ -87,22 +108,24 @@ public class CreateSubmission: APIUseCase {
public var scope: Scope { Scope(
predicate: NSPredicate(
format: "%K == %@ AND %K == %@",
#keyPath(Submission.assignmentID), assignmentID,
#keyPath(Submission.userID), userID
#keyPath(Submission.assignmentID), destination.assignmentID,
#keyPath(Submission.userID), destination.userID
),
orderBy: #keyPath(Submission.attempt),
ascending: false
) }

public func makeRequest(environment: AppEnvironment, completionHandler: @escaping (APISubmission?, URLResponse?, Error?) -> Void) {
environment.api.makeRequest(request) { [weak self] response, urlResponse, error in
guard let self = self else { return }
if error == nil {
NotificationCenter.default.post(moduleItem: .assignment(self.assignmentID), completedRequirement: .submit, courseID: self.context.id)
NotificationCenter.default.post(name: .moduleItemRequirementCompleted, object: nil)
apiCoordinator
.api(environment: environment)
.makeRequest(request) { [weak self] response, urlResponse, error in
guard let dest = self?.destination else { return }
if error == nil {
NotificationCenter.default.post(moduleItem: .assignment(dest.assignmentID), completedRequirement: .submit, courseID: dest.courseID)
NotificationCenter.default.post(name: .moduleItemRequirementCompleted, object: nil)
}
completionHandler(response, urlResponse, error)
}
completionHandler(response, urlResponse, error)
}
}

public func write(response: APISubmission?, urlResponse: URLResponse?, to client: NSManagedObjectContext) {
Expand All @@ -112,12 +135,46 @@ public class CreateSubmission: APIUseCase {
Submission.save(item, in: client)
if item.late != true {
NotificationCenter.default.post(name: .celebrateSubmission, object: nil, userInfo: [
"assignmentID": assignmentID
"assignmentID": destination.assignmentID
])
}
}
}

protocol SubmissionApiCoordinator: AnyObject {
func api(environment: AppEnvironment) -> API
}

class DefaultSubmissionApiCoordinator: SubmissionApiCoordinator {
private let destination: SubmissionDestination
private var api: API?

required init(destination: SubmissionDestination) {
self.destination = destination
}

func api(environment: AppEnvironment) -> API {
let shared = environment.api
guard let baseURL = destination.baseURL(in: environment) else { return shared }

if let api,
api.baseURL == baseURL,
api.loginSession == shared.loginSession,
api.urlSession == shared.urlSession {
return api
}

let instanceAPI = API(
shared.loginSession,
baseURL: baseURL,
urlSession: shared.urlSession
)

self.api = instanceAPI
return instanceAPI
}
}

public class GetSubmission: APIUseCase {
public let context: Context
public let assignmentID: String
Expand Down
16 changes: 8 additions & 8 deletions Core/CoreTests/Submissions/GetSubmissionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ class CreateSubmissionTests: CoreTestCase {
func testItCreatesAssignmentSubmission() {
// given
let submissionType = SubmissionType.online_url
let context = Context(.course, id: "1")
let url = URL(string: "http://www.instructure.com")!
let template: APISubmission = APISubmission.make(
assignment_id: "1",
Expand All @@ -71,7 +70,8 @@ class CreateSubmissionTests: CoreTestCase {
)

// when
let createSubmission = CreateSubmission(context: context, assignmentID: "1", userID: "1", submissionType: submissionType, url: url)
let dest = SubmissionDestination(courseID: "1", assignmentID: "1", userID: "1")
let createSubmission = CreateSubmission(destination: dest, submissionType: submissionType, url: url)
createSubmission.write(response: template, urlResponse: nil, to: databaseClient)

// then
Expand All @@ -88,8 +88,8 @@ class CreateSubmissionTests: CoreTestCase {
}

func testItPostsModuleCompletedRequirement() {
let context = Context(.course, id: "1")
let request = CreateSubmissionRequest(context: context, assignmentID: "2", body: .init(submission: .init(group_comment: nil, submission_type: .online_text_entry)))
let dest = SubmissionDestination(courseID: "1", assignmentID: "2", userID: "3")
let request = CreateSubmissionRequest(context: dest.context, assignmentID: "2", body: .init(submission: .init(group_comment: nil, submission_type: .online_text_entry)))
api.mock(request, value: nil)
let expectation = XCTestExpectation(description: "notification")
let token = NotificationCenter.default.addObserver(forName: .CompletedModuleItemRequirement, object: nil, queue: nil) { notification in
Expand All @@ -98,22 +98,22 @@ class CreateSubmissionTests: CoreTestCase {
XCTAssertEqual(notification.userInfo?["courseID"] as? String, "1")
expectation.fulfill()
}
let useCase = CreateSubmission(context: context, assignmentID: "2", userID: "3", submissionType: .online_text_entry)
let useCase = CreateSubmission(destination: dest, submissionType: .online_text_entry)
useCase.makeRequest(environment: environment) { _, _, _ in }
wait(for: [expectation], timeout: 0.5)
NotificationCenter.default.removeObserver(token)
}

func testItDoesNotPostModuleCompletedRequirementIfError() {
let context = Context(.course, id: "1")
let request = CreateSubmissionRequest(context: context, assignmentID: "2", body: .init(submission: .init(group_comment: nil, submission_type: .online_text_entry)))
let dest = SubmissionDestination(courseID: "1", assignmentID: "2", userID: "3")
let request = CreateSubmissionRequest(context: dest.context, assignmentID: "2", body: .init(submission: .init(group_comment: nil, submission_type: .online_text_entry)))
api.mock(request, error: NSError.instructureError("oops"))
let expectation = XCTestExpectation(description: "notification")
expectation.isInverted = true
let token = NotificationCenter.default.addObserver(forName: .CompletedModuleItemRequirement, object: nil, queue: nil) { _ in
expectation.fulfill()
}
let useCase = CreateSubmission(context: context, assignmentID: "2", userID: "3", submissionType: .online_text_entry)
let useCase = CreateSubmission(destination: dest, submissionType: .online_text_entry)
useCase.makeRequest(environment: environment) { _, _, _ in }
wait(for: [expectation], timeout: 0.2)
NotificationCenter.default.removeObserver(token)
Expand Down
77 changes: 77 additions & 0 deletions Core/CoreTests/Submissions/SubmissionDestinationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// This file is part of Canvas.
// Copyright (C) 2024-present Instructure, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//

import XCTest
@testable import Core

class SubmissionDestinationTests: CoreTestCase {

func test_api_resolving_for_dest_with_no_host() throws {
let destWithNoHost = SubmissionDestination(
courseID: "123",
assignmentID: "321",
userID: "456"
)

XCTAssertEqual(destWithNoHost.context, Context(.course, id: "123"))
XCTAssertNil(destWithNoHost.baseURL(in: environment))
}

func test_api_resolving_for_dest_with_host() throws {
let destWithHost = SubmissionDestination(
courseID: "456",
assignmentID: "876",
userID: "322",
apiInstanceHost: "canvas-034.instructure.com"
)

let expected = URL(string: "https://canvas-034.instructure.com")
XCTAssertEqual(destWithHost.baseURL(in: environment), expected)
XCTAssertEqual(destWithHost.context, Context(.course, id: "456"))
}
}

class DefaultSubmissionApiCoordinatorTests: CoreTestCase {

func test_api_resolving_with_no_host() throws {
let destWithNoHost = SubmissionDestination(
courseID: "123",
assignmentID: "321",
userID: "456"
)

let coordinator = DefaultSubmissionApiCoordinator(destination: destWithNoHost)
let api = coordinator.api(environment: environment)
XCTAssertEqual(api.baseURL, environment.api.baseURL)
XCTAssertEqual(api.loginSession, environment.api.loginSession)
}

func test_api_resolving_with_host() throws {
let destWithHost = SubmissionDestination(
courseID: "456",
assignmentID: "876",
userID: "322",
apiInstanceHost: "canvas-034.instructure.com"
)

let coordinator = DefaultSubmissionApiCoordinator(destination: destWithHost)
let api = coordinator.api(environment: environment)
XCTAssertEqual(api.baseURL, destWithHost.baseURL(in: environment))
XCTAssertEqual(api.loginSession, environment.api.loginSession)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ class AssignmentDetailsPresenter {
lazy var courses = env.subscribe(GetCourse(courseID: courseID)) { [weak self] in
self?.update()
}
lazy var tabs = env.subscribe(GetContextTabs(context: .course(courseID))) { [weak self] in
self?.updatePresenter()
}
lazy var features = env.subscribe(GetEnabledFeatureFlags(context: .course(courseID))) { [weak self] in
self?.updateSubmissionPickerButton()
self?.update()
Expand Down Expand Up @@ -153,6 +156,12 @@ class AssignmentDetailsPresenter {
}
}

func updatePresenter() {
guard let apiInstanceHost = tabs.first?.apiInstanceHost else { return }
guard env.api.baseURL.host() != apiInstanceHost else { return }
submissionButtonPresenter.apiInstanceHost = apiInstanceHost
}

func update() {
if submissions.requested == false || submissions.pending {
return
Expand Down Expand Up @@ -254,6 +263,7 @@ class AssignmentDetailsPresenter {
}

func viewIsReady() {
tabs.refresh()
colors.refresh()
courses.refresh(force: true)
assignments.refresh(force: true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,18 @@ protocol ArcSubmissionView: ErrorViewController {
class ArcSubmissionPresenter {
let env: AppEnvironment
weak var view: ArcSubmissionView?
let courseID: String
let assignmentID: String
let userID: String
let destination: SubmissionDestination
let arcID: String

init(environment: AppEnvironment = .shared, view: ArcSubmissionView, courseID: String, assignmentID: String, userID: String, arcID: String) {
init(environment: AppEnvironment = .shared, view: ArcSubmissionView, destination: SubmissionDestination, arcID: String) {
self.env = environment
self.view = view
self.courseID = courseID
self.assignmentID = assignmentID
self.userID = userID
self.destination = destination
self.arcID = arcID
}

func viewIsReady() {
let context = Context(.course, id: courseID)
let context = destination.context
let url = env.api.baseURL.appendingPathComponent("\(context.pathComponent)/external_tools/\(arcID)/resource_selection")
view?.load(url)
}
Expand All @@ -63,9 +59,7 @@ class ArcSubmissionPresenter {

func submit(url: URL, callback: @escaping (Error?) -> Void) {
CreateSubmission(
context: .course(courseID),
assignmentID: assignmentID,
userID: userID,
destination: destination,
submissionType: .basic_lti_launch,
url: url
).fetch(environment: env) { _, _, error in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,12 @@ class ArcSubmissionViewController: UIViewController, ArcSubmissionView {
weak var delegate: ArcSubmissionDelegate?
var presenter: ArcSubmissionPresenter?

static func create(environment: AppEnvironment = .shared, courseID: String, assignmentID: String, userID: String, arcID: String) -> ArcSubmissionViewController {
static func create(environment: AppEnvironment = .shared, destination: SubmissionDestination, arcID: String) -> ArcSubmissionViewController {
let controller = loadFromStoryboard()
let presenter = ArcSubmissionPresenter(
environment: environment,
view: controller,
courseID: courseID,
assignmentID: assignmentID,
userID: userID,
destination: destination,
arcID: arcID
)
controller.presenter = presenter
Expand Down
Loading