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

Development: Create automatic screenshots using Fastlane #263

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
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,13 +16,18 @@
}

/// 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!!!

Check warning on line 20 in Themis/Extensions/ParticipationExtension.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Todo Violation: TODOs should be resolved (I think this works, but please...). (todo)
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
func getId(for repoType: RepositoryType) -> Int? {
guard let _ = (self as? ProgrammingExerciseStudentParticipation) else {

Check warning on line 30 in Themis/Extensions/ParticipationExtension.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Unused Optional Binding Violation: Prefer `!= nil` over `let _ =` (unused_optional_binding)
return nil
}

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 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/")!))

Check warning on line 12 in Themis/REST/RESTController.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Force Unwrapping Violation: Force unwrapping should be avoided. (force_unwrapping)

Check warning on line 12 in Themis/REST/RESTController.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Force Unwrapping Violation: Force unwrapping should be avoided. (force_unwrapping)

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 @@ -207,7 +207,7 @@
}
}

func notifyThemisML() async { // TODO: Make this function more general once Athene is integrated

Check warning on line 210 in Themis/ViewModels/Assessment/AssessmentViewModel.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Todo Violation: TODOs should be resolved (Make this function more genera...). (todo)
guard let participationId = participation?.id,
case .programming = exercise
else {
Expand Down Expand Up @@ -235,19 +235,19 @@
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
Loading