Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
430 changes: 352 additions & 78 deletions Themis.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions Themis/Extensions/ParticipationExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ extension BaseParticipation {
}

/// A convenience function that casts this BaseParticipation instance into a ProgrammingExerciseStudentParticipation and sets its exercise property
func setProgrammingExercise(_ exercise: Exercise) {
(self as? ProgrammingExerciseStudentParticipation)?.exercise = exercise
mutating func setProgrammingExercise(_ exercise: Exercise) {
// TODO: I think this works, but please test!!!
if var programmingParticipation = self as? ProgrammingExerciseStudentParticipation {
programmingParticipation.exercise = exercise
// swiftlint:disable:next force_cast
self = programmingParticipation as! Self
}
}

/// A convenience function that casts this BaseParticipation instance into a ProgrammingExerciseStudentParticipation and returns the participation id for the given repository type
Expand Down
2 changes: 1 addition & 1 deletion Themis/REST/RESTController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation
import UserStore

class RESTController {
static var shared = RESTController(baseURL: (UserSession.shared.institution?.baseURL ?? URL(string: "https://artemis.ase.cit.tum.de/")!))
static var shared = RESTController(baseURL: (UserSessionFactory.shared.institution?.baseURL ?? URL(string: "https://artemis.ase.cit.tum.de/")!))

var baseURL: URL

Expand Down
5 changes: 3 additions & 2 deletions Themis/Services/Repository/RepositoryService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ protocol RepositoryService {
func getFileOfRepository(participationId: Int, filePath: String) async throws -> String
}

enum RepositoryServiceFactory {
static let shared: RepositoryService = RepositoryServiceImpl()
enum RepositoryServiceFactory: DependencyFactory {
static let liveValue: RepositoryService = RepositoryServiceImpl()
static let testValue: RepositoryService = RepositoryServiceStub()
}
68 changes: 68 additions & 0 deletions Themis/Services/Repository/RepositoryServiceStub.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// RepositoryServiceStub.swift
// Themis
//
// Created by Anian Schleyer on 09.06.24.
//

import SharedModels

class RepositoryServiceStub: RepositoryService {
func getFileNamesOfRepository(participationId: Int) async throws -> [String: FileType] {
[
"code.swift": .file,
"assets": .folder
]
}

func getFileOfRepository(participationId: Int, filePath: String) async throws -> String {
// Using code from ThemisButtonStyle.swift as example
"""
//
// ThemisButtonStyle.swift
// Themis
//
// Created by Andreas Cselovszky on 10.01.23.
//

import SwiftUI

struct ThemisButtonStyle: ButtonStyle {
@Environment(\\.isEnabled) var isEnabled: Bool
var color = Color.themisSecondary
var iconImageName: String?
var horizontalPadding: CGFloat = 15
var verticalPadding: CGFloat = 8

func makeBody(configuration: Configuration) -> some View {
HStack {
if let iconImageName {
Image(iconImageName)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 14, height: 6)
.foregroundColor(.white)
}

configuration.label
}
.foregroundColor(Color(.systemBackground))
.padding(.horizontal, horizontalPadding)
.padding(.vertical, verticalPadding)
.background(isEnabled ? color : Color(.systemGray))
.cornerRadius(5)
.fontWeight(.semibold)
.scaleEffect(configuration.isPressed ? 1.1 : 1)
.animation(.easeOut(duration: 0.2), value: configuration.isPressed)
}
}

struct NavigationBarButtonStyle_Previews: PreviewProvider {
static var previews: some View {
Button("Save", action: {})
.buttonStyle(ThemisButtonStyle(iconImageName: "saveIcon"))
}
}
"""
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// ProgrammingSubmissionService.swift
// Themis
//
// Created by Anian Schleyer on 09.06.24.
//

import Common

enum ProgrammingSubmissionServiceFactory: DependencyFactory {
static let liveValue: any SubmissionService = ProgrammingSubmissionServiceImpl()
static let testValue: any SubmissionService = ProgrammingSubmissionServiceStub()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// ProgrammingSubmissionServiceStub.swift
// Themis
//
// Created by Anian Schleyer on 09.06.24.
//

import SharedModels

class ProgrammingSubmissionServiceStub: SubmissionService {
func getAllSubmissions(exerciseId: Int) async throws -> [Submission] {
[.programming(submission: .mock)]
}

func getTutorSubmissions(exerciseId: Int, correctionRound: CorrectionRound) async throws -> [Submission] {
[.programming(submission: .mock)]
}

func getRandomSubmissionForAssessment(exerciseId: Int, correctionRound: CorrectionRound) async throws -> ProgrammingSubmission {
.mock
}

func getSubmissionForAssessment(submissionId: Int, correctionRound: CorrectionRound) async throws -> ProgrammingSubmission {
.mock
}

typealias SubmissionType = ProgrammingSubmission
}
2 changes: 1 addition & 1 deletion Themis/Services/Submission/SubmissionService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ enum SubmissionServiceFactory {
static func service(for exercise: Exercise) -> any SubmissionService {
switch exercise {
case .programming:
return ProgrammingSubmissionServiceImpl()
return ProgrammingSubmissionServiceFactory.shared
case .text:
return TextSubmissionServiceImpl()
case .modeling:
Expand Down
18 changes: 9 additions & 9 deletions Themis/ViewModels/Assessment/AssessmentViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -235,19 +235,19 @@ enum AssessmentViewModelFactory {
readOnly: Bool) -> AssessmentViewModel {
switch exercise {
case .programming:
return ProgrammingAssessmentViewModel(exercise: exercise,
return ProgrammingAssessmentViewModelFactory(exercise: exercise,
submissionId: submissionId,
participationId: participationId,
resultId: resultId,
correctionRound: correctionRound,
readOnly: readOnly).shared
case .text:
return TextAssessmentViewModelFactory(exercise: exercise,
submissionId: submissionId,
participationId: participationId,
resultId: resultId,
correctionRound: correctionRound,
readOnly: readOnly)
case .text:
return TextAssessmentViewModel(exercise: exercise,
submissionId: submissionId,
participationId: participationId,
resultId: resultId,
correctionRound: correctionRound,
readOnly: readOnly)
readOnly: readOnly).shared
case .modeling:
return ModelingAssessmentViewModel(exercise: exercise,
submissionId: submissionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class FileRendererViewModel: ExerciseRendererViewModel {
@MainActor
func setup(basedOn submission: BaseSubmission? = nil) async {
guard let fileUploadSubmission = submission as? FileUploadSubmission,
let baseUrl = UserSession.shared.institution?.baseURL?.absoluteString,
let baseUrl = UserSessionFactory.shared.institution?.baseURL?.absoluteString,
let filePath = fileUploadSubmission.filePath?.dropFirst(),
let remoteFileUrl = URL(string: "\(baseUrl)\(filePath)") else {
log.error("Setup failed")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,78 +2,40 @@
// ProgrammingAssessmentViewModel.swift
// Themis
//
// Created by Tarlan Ismayilsoy on 29.07.23.
// Created by Anian Schleyer on 09.06.24.
//

import Foundation
import Common
import SharedModels

class ProgrammingAssessmentViewModel: AssessmentViewModel {
@MainActor
override func initSubmission() async {
guard submission == nil else {
return
}

if readOnly {
if let participationId {
await getReadOnlySubmission(participationId: participationId)
} else {
self.error = UserFacingError.participationNotFound
log.error("Could not find participation for programming exercise: \(exercise.baseExercise.title ?? "")")
}
} else {
if let submissionId {
await getSubmission(submissionId: submissionId)
} else {
await initRandomSubmission()
}
}

ThemisUndoManager.shared.removeAllActions()
}

@MainActor
override func getReadOnlySubmission(participationId: Int) async {
guard readOnly else {
self.error = UserFacingError.unknown
log.error("This function should only be called for read-only mode")
return
}

loading = true
defer {
loading = false
}

let submissionService = SubmissionServiceFactory.service(for: exercise)

do {
let result = try await submissionService.getResultFor(participationId: participationId)
self.submission = result.submission?.baseSubmission
self.participation = result.participation?.baseParticipation
assessmentResult.setComputedFeedbacks(basedOn: result.feedbacks ?? [])

if let exerciseId = participation?.exercise?.id {
let exerciseWithTemplateAndSolution = try await ExerciseHelperService()
.getProgrammingExerciseWithTemplateAndSolutionParticipations(exerciseId: exerciseId)
self.participation?.setProgrammingExercise(exerciseWithTemplateAndSolution)
}
} catch {
self.error = error
log.error(String(describing: error))
}
protocol ProgrammingAssessmentViewModel {
func participationId(for repoType: RepositoryType) -> Int?
}

struct ProgrammingAssessmentViewModelFactory: DependencyFactory {
init(exercise: Exercise, submissionId: Int?, participationId: Int?, resultId: Int?, correctionRound: CorrectionRound, readOnly: Bool) {
Self.liveValue = ProgrammingAssessmentViewModelImpl(
exercise: exercise,
submissionId: submissionId,
participationId: participationId,
resultId: resultId,
correctionRound: correctionRound,
readOnly: readOnly
)
Self.testValue = ProgrammingAssessmentViewModelStub(
exercise: exercise,
submissionId: submissionId,
participationId: participationId,
resultId: resultId,
correctionRound: correctionRound,
readOnly: readOnly
)
}

func participationId(for repoType: RepositoryType) -> Int? {
switch repoType {
case .student:
return participation?.id
case .solution:
return participation?.getExercise(as: ProgrammingExercise.self)?.solutionParticipation?.id
case .template:
return participation?.getExercise(as: ProgrammingExercise.self)?.templateParticipation?.id
}
var shared: AssessmentViewModel {
Self.shared
}

static var liveValue: AssessmentViewModel = ProgrammingAssessmentViewModelImpl(exercise: .mockText, correctionRound: .first, readOnly: true)
static var testValue: AssessmentViewModel = ProgrammingAssessmentViewModelImpl(exercise: .mockText, correctionRound: .first, readOnly: true)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// ProgrammingAssessmentViewModelImpl.swift
// Themis
//
// Created by Tarlan Ismayilsoy on 29.07.23.
//

import Foundation
import Common
import SharedModels

class ProgrammingAssessmentViewModelImpl: AssessmentViewModel, ProgrammingAssessmentViewModel {
@MainActor
override func initSubmission() async {
guard submission == nil else {
return
}

if readOnly {
if let participationId {
await getReadOnlySubmission(participationId: participationId)
} else {
self.error = UserFacingError.participationNotFound
log.error("Could not find participation for programming exercise: \(exercise.baseExercise.title ?? "")")
}
} else {
if let submissionId {
await getSubmission(submissionId: submissionId)
} else {
await initRandomSubmission()
}
}

ThemisUndoManager.shared.removeAllActions()
}

@MainActor
override func getReadOnlySubmission(participationId: Int) async {
guard readOnly else {
self.error = UserFacingError.unknown
log.error("This function should only be called for read-only mode")
return
}

loading = true
defer {
loading = false
}

let submissionService = SubmissionServiceFactory.service(for: exercise)

do {
let result = try await submissionService.getResultFor(participationId: participationId)
self.submission = result.submission?.baseSubmission
self.participation = result.participation?.baseParticipation
assessmentResult.setComputedFeedbacks(basedOn: result.feedbacks ?? [])

if let exerciseId = participation?.exercise?.id {
let exerciseWithTemplateAndSolution = try await ExerciseHelperService()
.getProgrammingExerciseWithTemplateAndSolutionParticipations(exerciseId: exerciseId)
self.participation?.setProgrammingExercise(exerciseWithTemplateAndSolution)
}
} catch {
self.error = error
log.error(String(describing: error))
}
}

func participationId(for repoType: RepositoryType) -> Int? {
switch repoType {
case .student:
return participation?.id
case .solution:
return participation?.getExercise(as: ProgrammingExercise.self)?.solutionParticipation?.id
case .template:
return participation?.getExercise(as: ProgrammingExercise.self)?.templateParticipation?.id
}
}
}
Loading