module Client.Features.Teacher.Tests.State open Elmish open Domain.Contracts open Client.Features.Teacher.Tests.Types let init () : Model * Cmd = empty, Cmd.batch [ Cmd.ofMsg LoadQuizzes; Cmd.ofMsg LoadStudents; Cmd.ofMsg LoadTopics ] /// Empty string means "not set" for these optional numeric fields; a /// non-empty string that fails to parse is a validation error. let private parseOptionalInt (text: string) : Result = if System.String.IsNullOrWhiteSpace text then Ok None else match System.Int32.TryParse text with | true, v -> Ok(Some v) | false, _ -> Error "Ожидалось целое число" let private parseOptionalFloat (text: string) : Result = if System.String.IsNullOrWhiteSpace text then Ok None else match System.Double.TryParse text with | true, v -> Ok(Some v) | false, _ -> Error "Ожидалось число" let private buildQuizFields (form: QuizForm) = match parseOptionalInt form.TimeLimitText, parseOptionalInt form.MaxAttemptsText, parseOptionalFloat form.PassingScoreText with | Error err, _, _ | _, Error err, _ | _, _, Error err -> Error err | Ok timeLimit, Ok maxAttempts, Ok passingScore -> if System.String.IsNullOrWhiteSpace form.Title then Error "Название теста не может быть пустым" elif form.Sources.IsEmpty then Error "Выберите хотя бы один вопрос" else Ok(timeLimit, maxAttempts, passingScore) let private toDraft (source: QuizQuestionSourceInput) : QuizSourceDraft = match source with | FixedQuestionInput qid -> FixedDraft qid | RandomPoolInput rule -> PoolDraft(rule.TopicId, rule.Count) let private toSourceInput (draft: QuizSourceDraft) : QuizQuestionSourceInput = match draft with | FixedDraft qid -> FixedQuestionInput qid | PoolDraft(topicId, count) -> RandomPoolInput { TopicId = topicId; Count = count } let update (token: string option) (msg: Msg) (model: Model) : Model * Cmd = match msg with | LoadQuizzes -> let cmd = Cmd.OfAsync.either Api.listMyQuizzes token (function | Ok quizzes -> QuizzesLoaded quizzes | Error err -> QuizzesLoadFailed err) (fun ex -> QuizzesLoadFailed ex.Message) { model with QuizzesLoading = true; QuizzesError = None }, cmd | QuizzesLoaded quizzes -> { model with Quizzes = quizzes; QuizzesLoading = false }, Cmd.none | QuizzesLoadFailed err -> { model with QuizzesLoading = false; QuizzesError = Some err }, Cmd.none | LoadStudents -> let cmd = Cmd.OfAsync.either Api.listStudents token (function | Ok students -> StudentsLoaded students | Error err -> StudentsLoadFailed err) (fun ex -> StudentsLoadFailed ex.Message) { model with StudentsLoading = true; StudentsError = None }, cmd | StudentsLoaded students -> { model with Students = students; StudentsLoading = false }, Cmd.none | StudentsLoadFailed err -> { model with StudentsLoading = false; StudentsError = Some err }, Cmd.none | SelectQuiz quizId -> let assigned = model.Quizzes |> List.tryFind (fun q -> q.Id = quizId) |> Option.map (fun q -> Set.ofList q.AssignedStudentIds) |> Option.defaultValue Set.empty { model with SelectedQuizId = Some quizId SelectedStudentIds = assigned SaveError = None ShowQuizForm = false ResultsQuizId = None }, Cmd.none | ToggleStudent studentId -> let next = if model.SelectedStudentIds.Contains studentId then model.SelectedStudentIds.Remove studentId else model.SelectedStudentIds.Add studentId { model with SelectedStudentIds = next }, Cmd.none | SaveAssignments -> match model.SelectedQuizId with | None -> model, Cmd.none | Some quizId -> let request: AssignStudentsRequest = { QuizId = quizId StudentIds = Set.toList model.SelectedStudentIds } let cmd = Cmd.OfAsync.either (Api.assignStudents token) request (function | Ok summary -> AssignmentsSaved summary | Error err -> SaveFailed err) (fun ex -> SaveFailed ex.Message) { model with IsSaving = true; SaveError = None }, cmd | AssignmentsSaved summary -> { model with Quizzes = model.Quizzes |> List.map (fun q -> if q.Id = summary.Id then summary else q) IsSaving = false }, Cmd.none | SaveFailed err -> { model with IsSaving = false; SaveError = Some err }, Cmd.none | LoadTopics -> let cmd = Cmd.OfAsync.either Api.listTopics token (function | Ok topics -> TopicsLoaded topics | Error err -> TopicsLoadFailed err) (fun ex -> TopicsLoadFailed ex.Message) { model with TopicsLoading = true; TopicsError = None }, cmd | TopicsLoaded topics -> { model with Topics = topics; TopicsLoading = false }, Cmd.none | TopicsLoadFailed err -> { model with TopicsLoading = false; TopicsError = Some err }, Cmd.none | StartNewQuiz -> { model with ShowQuizForm = true EditingQuizId = None QuizForm = emptyQuizForm SelectedQuizId = None ResultsQuizId = None }, Cmd.none | StartEditQuiz quizId -> match model.Quizzes |> List.tryFind (fun q -> q.Id = quizId) with | None -> model, Cmd.none | Some quiz -> let form = { emptyQuizForm with Title = quiz.Title Description = quiz.Description TimeLimitText = quiz.TimeLimitMinutes |> Option.map string |> Option.defaultValue "" MaxAttemptsText = quiz.MaxAttempts |> Option.map string |> Option.defaultValue "" PassingScoreText = quiz.PassingScore |> Option.map string |> Option.defaultValue "" ShuffleQuestions = quiz.ShuffleQuestions ShuffleAnswers = quiz.ShuffleAnswers Sources = quiz.Sources |> List.map toDraft } { model with ShowQuizForm = true EditingQuizId = Some quizId QuizForm = form }, Cmd.none | CancelQuizForm -> { model with ShowQuizForm = false; EditingQuizId = None }, Cmd.none | SetQuizTitle text -> { model with QuizForm = { model.QuizForm with Title = text } }, Cmd.none | SetQuizDescription text -> { model with QuizForm = { model.QuizForm with Description = text } }, Cmd.none | SetQuizTimeLimitText text -> { model with QuizForm = { model.QuizForm with TimeLimitText = text } }, Cmd.none | SetQuizMaxAttemptsText text -> { model with QuizForm = { model.QuizForm with MaxAttemptsText = text } }, Cmd.none | SetQuizPassingScoreText text -> { model with QuizForm = { model.QuizForm with PassingScoreText = text } }, Cmd.none | SetQuizShuffleQuestions value -> { model with QuizForm = { model.QuizForm with ShuffleQuestions = value } }, Cmd.none | SetQuizShuffleAnswers value -> { model with QuizForm = { model.QuizForm with ShuffleAnswers = value } }, Cmd.none | ToggleTopicInPicker topicId -> let form = model.QuizForm if form.PickerTopicIds.Contains topicId then // Deselecting a topic drops it from the quiz entirely — its // fixed picks and pool rule no longer apply. let belongsToTopic qid = form.PickerQuestions |> List.tryFind (fun q -> q.Id = qid) |> Option.map (fun q -> q.TopicId = topicId) |> Option.defaultValue false let sourcesWithoutTopic = form.Sources |> List.filter (function | PoolDraft(tid, _) -> tid <> topicId | FixedDraft qid -> not (belongsToTopic qid)) { model with QuizForm = { form with PickerTopicIds = form.PickerTopicIds.Remove topicId PickerQuestions = form.PickerQuestions |> List.filter (fun q -> q.TopicId <> topicId) PoolCountTexts = form.PoolCountTexts.Remove topicId Sources = sourcesWithoutTopic } }, Cmd.none else let cmd = Cmd.OfAsync.either (Api.listQuestionsInTopic token) topicId (function | Ok questions -> PickerQuestionsLoaded(topicId, questions) | Error err -> PickerQuestionsLoadFailed(topicId, err)) (fun ex -> PickerQuestionsLoadFailed(topicId, ex.Message)) let existingPoolCount = form.Sources |> List.tryPick (function | PoolDraft(tid, count) when tid = topicId -> Some count | _ -> None) let poolCountTexts = match existingPoolCount with | Some count -> form.PoolCountTexts |> Map.add topicId (string count) | None -> form.PoolCountTexts { model with QuizForm = { form with PickerTopicIds = form.PickerTopicIds.Add topicId PickerLoadingTopics = form.PickerLoadingTopics.Add topicId PoolCountTexts = poolCountTexts } }, cmd | PickerQuestionsLoaded(topicId, questions) -> { model with QuizForm = { model.QuizForm with PickerQuestions = model.QuizForm.PickerQuestions @ questions PickerLoadingTopics = model.QuizForm.PickerLoadingTopics.Remove topicId } }, Cmd.none | PickerQuestionsLoadFailed(topicId, err) -> { model with QuizForm = { model.QuizForm with PickerLoadingTopics = model.QuizForm.PickerLoadingTopics.Remove topicId Error = Some err } }, Cmd.none | ToggleQuestionPick questionId -> let form = model.QuizForm let next = if form.Sources |> List.exists (function FixedDraft qid -> qid = questionId | _ -> false) then form.Sources |> List.filter (function | FixedDraft qid -> qid <> questionId | _ -> true) else form.Sources @ [ FixedDraft questionId ] { model with QuizForm = { form with Sources = next } }, Cmd.none | SelectAllInTopic topicId -> let form = model.QuizForm let alreadySelected = form.Sources |> List.choose (function | FixedDraft qid -> Some qid | _ -> None) |> Set.ofList let toAdd = form.PickerQuestions |> List.filter (fun q -> q.TopicId = topicId) |> List.map (fun q -> q.Id) |> List.filter (alreadySelected.Contains >> not) |> List.map FixedDraft { model with QuizForm = { form with Sources = form.Sources @ toAdd } }, Cmd.none | SetPoolCountText(topicId, text) -> { model with QuizForm = { model.QuizForm with PoolCountTexts = model.QuizForm.PoolCountTexts |> Map.add topicId text } }, Cmd.none | AddRandomPool topicId -> let form = model.QuizForm let text = form.PoolCountTexts |> Map.tryFind topicId |> Option.defaultValue "" match System.Int32.TryParse text with | true, count when count > 0 -> let withoutOldPool = form.Sources |> List.filter (function | PoolDraft(tid, _) -> tid <> topicId | _ -> true) { model with QuizForm = { form with Sources = withoutOldPool @ [ PoolDraft(topicId, count) ]; Error = None } }, Cmd.none | _ -> { model with QuizForm = { form with Error = Some "Количество случайных вопросов должно быть положительным числом" } }, Cmd.none | RemoveRandomPool topicId -> let form = model.QuizForm let next = form.Sources |> List.filter (function | PoolDraft(tid, _) -> tid <> topicId | _ -> true) { model with QuizForm = { form with Sources = next; PoolCountTexts = form.PoolCountTexts.Remove topicId } }, Cmd.none | SubmitQuizForm -> match buildQuizFields model.QuizForm with | Error err -> { model with QuizForm = { model.QuizForm with Error = Some err } }, Cmd.none | Ok(timeLimit, maxAttempts, passingScore) -> let form = { model.QuizForm with IsSubmitting = true; Error = None } let sources = form.Sources |> List.map toSourceInput let cmd = match model.EditingQuizId with | None -> let req: CreateQuizRequest = { Title = form.Title Description = form.Description TimeLimitMinutes = timeLimit MaxAttempts = maxAttempts PassingScore = passingScore ShuffleQuestions = form.ShuffleQuestions ShuffleAnswers = form.ShuffleAnswers Sources = sources } Cmd.OfAsync.either (Api.createQuiz token) req (function | Ok summary -> QuizSaved summary | Error err -> QuizSaveFailed err) (fun ex -> QuizSaveFailed ex.Message) | Some quizId -> let req: UpdateQuizRequest = { QuizId = quizId Title = form.Title Description = form.Description TimeLimitMinutes = timeLimit MaxAttempts = maxAttempts PassingScore = passingScore ShuffleQuestions = form.ShuffleQuestions ShuffleAnswers = form.ShuffleAnswers Sources = sources } Cmd.OfAsync.either (Api.updateQuiz token) req (function | Ok summary -> QuizSaved summary | Error err -> QuizSaveFailed err) (fun ex -> QuizSaveFailed ex.Message) { model with QuizForm = form }, cmd | QuizSaved summary -> let alreadyExists = model.Quizzes |> List.exists (fun q -> q.Id = summary.Id) { model with Quizzes = if alreadyExists then model.Quizzes |> List.map (fun q -> if q.Id = summary.Id then summary else q) else model.Quizzes @ [ summary ] ShowQuizForm = false EditingQuizId = None }, Cmd.none | QuizSaveFailed err -> { model with QuizForm = { model.QuizForm with IsSubmitting = false; Error = Some err } }, Cmd.none | RequestDeleteQuiz quizId -> let cmd = Cmd.OfAsync.either (Api.deleteQuiz token) quizId (function | Ok() -> QuizDeleted quizId | Error err -> QuizDeleteFailed err) (fun ex -> QuizDeleteFailed ex.Message) model, cmd | QuizDeleted quizId -> { model with Quizzes = model.Quizzes |> List.filter (fun q -> q.Id <> quizId) SelectedQuizId = if model.SelectedQuizId = Some quizId then None else model.SelectedQuizId EditingQuizId = if model.EditingQuizId = Some quizId then None else model.EditingQuizId ShowQuizForm = if model.EditingQuizId = Some quizId then false else model.ShowQuizForm }, Cmd.none | QuizDeleteFailed err -> { model with QuizzesError = Some err }, Cmd.none | ShowResults quizId -> let cmd = Cmd.OfAsync.either (Api.getQuizResults token) quizId (function | Ok results -> ResultsLoaded results | Error err -> ResultsLoadFailed err) (fun ex -> ResultsLoadFailed ex.Message) { model with ResultsQuizId = Some quizId ResultsLoading = true ResultsError = None Results = [] SelectedQuizId = None ShowQuizForm = false }, cmd | ResultsLoaded results -> { model with Results = results; ResultsLoading = false }, Cmd.none | ResultsLoadFailed err -> { model with ResultsLoading = false; ResultsError = Some err }, Cmd.none | CancelResults -> { model with ResultsQuizId = None }, Cmd.none