diff --git a/Management.Test/Markdown/Quiz/MatchingTests.cs b/Management.Test/Markdown/Quiz/MatchingTests.cs index 11fcd14..601aba7 100644 --- a/Management.Test/Markdown/Quiz/MatchingTests.cs +++ b/Management.Test/Markdown/Quiz/MatchingTests.cs @@ -101,7 +101,7 @@ Match the following terms & definitions "; var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz); - quiz.Questions.First().Answers.First().MatchDistractors.Should().BeEquivalentTo(["this is the distractor"]); + quiz.Questions.First().MatchDistractors.Should().BeEquivalentTo(["this is the distractor"]); } [Fact] public void CanHaveDistractorsAndBePersisted() @@ -118,7 +118,7 @@ Description: --- Match the following terms & definitions -^statement - a single command to be executed +^ statement - a single command to be executed ^ - this is the distractor "; diff --git a/Management.Test/Markdown/Quiz/QuizDeterministicChecks.cs b/Management.Test/Markdown/Quiz/QuizDeterministicChecks.cs index a675b6c..9348110 100644 --- a/Management.Test/Markdown/Quiz/QuizDeterministicChecks.cs +++ b/Management.Test/Markdown/Quiz/QuizDeterministicChecks.cs @@ -195,8 +195,7 @@ public class QuizDeterministicChecks Text = "test matching", QuestionType = QuestionType.MATCHING, Points = 1, - Answers = new LocalQuizQuestionAnswer[] - { + Answers = [ new() { Correct = true, Text="yes", @@ -207,7 +206,7 @@ public class QuizDeterministicChecks Text="no", MatchedText = "testing no" } - } + ] } } }; diff --git a/Management/Models/Local/Quiz/LocalQuizQuestion.cs b/Management/Models/Local/Quiz/LocalQuizQuestion.cs index c81eec2..ef7b7b1 100644 --- a/Management/Models/Local/Quiz/LocalQuizQuestion.cs +++ b/Management/Models/Local/Quiz/LocalQuizQuestion.cs @@ -13,10 +13,17 @@ public record LocalQuizQuestion public double Points { get; init; } public IEnumerable Answers { get; init; } = Enumerable.Empty(); + public IEnumerable MatchDistractors { get; init; } = []; public string ToMarkdown() { var answerArray = Answers.Select(getAnswerMarkdown); - var answersText = string.Join("\n", answerArray); + + + var distractorText = MatchDistractors + .Select(d => $"\n^ - {d}") + .Join(""); + + var answersText = string.Join("\n", answerArray) + distractorText; var questionTypeIndicator = QuestionType == "essay" || QuestionType == "short_answer" ? QuestionType : ""; return $@"Points: {Points} @@ -39,10 +46,7 @@ public record LocalQuizQuestion } else if (QuestionType == "matching") { - var distractorText = answer.MatchDistractors?.Select( - d => $"\n^ - {d}" - ).Join("") ?? ""; - return $"^ {answer.Text} - {answer.MatchedText}" + distractorText; + return $"^ {answer.Text} - {answer.MatchedText}"; } else { @@ -98,12 +102,22 @@ public record LocalQuizQuestion ? getAnswers(linesWithoutPoints, questionIndex, questionType) : []; + var distractors = questionType == "matching" + ? answers.Where(a => a.Text == "").Select(a => a.MatchedText ?? "").ToArray() + : []; + + var answersWithoutDistractors = questionType == "matching" + ? answers.Where(a => a.Text != "").ToArray() + : answers; + + return new LocalQuizQuestion() { Text = description, Points = points, - Answers = answers, - QuestionType = questionType + Answers = answersWithoutDistractors, + QuestionType = questionType, + MatchDistractors = distractors }; } @@ -184,27 +198,6 @@ public record LocalQuizQuestion var answers = answerLines .Select((a, i) => LocalQuizQuestionAnswer.ParseMarkdown(a, questionType)) - .Aggregate([], (IEnumerable accumulator, LocalQuizQuestionAnswer answer) => - { - if (questionType != "matching") - return accumulator.Append(answer); - - if (accumulator.Count() == 0) - return accumulator.Append(answer); - - if (answer.Text != "") - return accumulator.Append(answer); - - - var previousDistractors = accumulator.Last().MatchDistractors ?? []; - var newLastAnswer = accumulator.Last() with - { - MatchDistractors = previousDistractors.Append(answer.MatchedText ?? "").ToArray() - }; - - return accumulator.Reverse().Skip(1).Reverse().Append(newLastAnswer); - - }) .ToArray(); return answers; diff --git a/Management/Models/Local/Quiz/LocalQuizQuestionAnswer.cs b/Management/Models/Local/Quiz/LocalQuizQuestionAnswer.cs index 5e29d71..1b2e71c 100644 --- a/Management/Models/Local/Quiz/LocalQuizQuestionAnswer.cs +++ b/Management/Models/Local/Quiz/LocalQuizQuestionAnswer.cs @@ -9,7 +9,6 @@ public record LocalQuizQuestionAnswer public string Text { get; init; } = string.Empty; public string? MatchedText { get; init; } - public IEnumerable? MatchDistractors { get; init; } public string HtmlText => MarkdownService.Render(Text); diff --git a/Management/Services/Canvas/CanvasQuizService.cs b/Management/Services/Canvas/CanvasQuizService.cs index bc03685..5458f09 100644 --- a/Management/Services/Canvas/CanvasQuizService.cs +++ b/Management/Services/Canvas/CanvasQuizService.cs @@ -1,5 +1,7 @@ using CanvasModel.Quizzes; + using LocalModels; + using RestSharp; namespace Management.Services.Canvas; @@ -16,7 +18,7 @@ public class CanvasQuizService( CanvasServiceUtils utils, ICanvasAssignmentService assignments, ILogger logger -): ICanvasQuizService +) : ICanvasQuizService { private readonly IWebRequestor webRequestor = webRequestor; private readonly CanvasServiceUtils utils = utils; @@ -164,6 +166,7 @@ public class CanvasQuizService( var url = $"courses/{canvasCourseId}/quizzes/{canvasQuizId}/questions"; var answers = getAnswers(q); + var body = new { question = new @@ -172,9 +175,12 @@ public class CanvasQuizService( question_type = q.QuestionType + "_question", points_possible = q.Points, position, + matching_answer_incorrect_matches = string.Join("\n", q.MatchDistractors), answers } }; + Console.WriteLine(JsonSerializer.Serialize(q)); + Console.WriteLine(JsonSerializer.Serialize(body)); var request = new RestRequest(url); request.AddBody(body); @@ -197,7 +203,6 @@ public class CanvasQuizService( { answer_match_left = a.Text, answer_match_right = a.MatchedText, - matching_answer_incorrect_matches = a.MatchDistractors, }) .ToArray(); diff --git a/nextjs/src/models/local/assignmnet/localAssignment.ts b/nextjs/src/models/local/assignmnet/localAssignment.ts index 339f0a4..143e16f 100644 --- a/nextjs/src/models/local/assignmnet/localAssignment.ts +++ b/nextjs/src/models/local/assignmnet/localAssignment.ts @@ -4,8 +4,8 @@ import { RubricItem } from "./rubricItem"; export interface LocalAssignment { name: string; description: string; - lockAt?: string; // ISO 8601 date string - dueAt: string; // ISO 8601 date string + lockAt?: string; // 21/08/2023 23:59:00 + dueAt: string; // 21/08/2023 23:59:00 localAssignmentGroupName?: string; submissionTypes: AssignmentSubmissionType[]; allowedFileUploadExtensions: string[]; diff --git a/nextjs/src/models/local/assignmnet/utils/assignmentMarkdownParser.ts b/nextjs/src/models/local/assignmnet/utils/assignmentMarkdownParser.ts index 88ae281..0def561 100644 --- a/nextjs/src/models/local/assignmnet/utils/assignmentMarkdownParser.ts +++ b/nextjs/src/models/local/assignmnet/utils/assignmentMarkdownParser.ts @@ -1,3 +1,4 @@ +import { timeUtils } from "../../timeUtils"; import { AssignmentSubmissionType } from "../assignmentSubmissionType"; import { LocalAssignment } from "../localAssignment"; import { RubricItem } from "../rubricItem"; @@ -50,12 +51,8 @@ const parseSettings = (input: string) => { const submissionTypes = parseSubmissionTypes(input); const fileUploadExtensions = parseFileUploadExtensions(input); - const lockAt = (rawLockAt ? new Date(rawLockAt) : undefined)?.toISOString(); - const dueAt = new Date(rawDueAt).toISOString(); - - if (isNaN(new Date(dueAt).getTime())) { - throw new Error(`Error with DueAt: ${rawDueAt}`); - } + const dueAt = timeUtils.parseDateOrThrow(rawDueAt, "DueAt"); + const lockAt = timeUtils.parseDateOrUndefined(rawLockAt); return { name, diff --git a/nextjs/src/models/local/quiz/utils/quizMarkdownUtils.ts b/nextjs/src/models/local/quiz/utils/quizMarkdownUtils.ts index 07c8faa..51ed193 100644 --- a/nextjs/src/models/local/quiz/utils/quizMarkdownUtils.ts +++ b/nextjs/src/models/local/quiz/utils/quizMarkdownUtils.ts @@ -1,3 +1,4 @@ +import { timeUtils } from "../../timeUtils"; import { LocalQuiz } from "../localQuiz"; import { quizQuestionMarkdownUtils } from "./quizQuestionMarkdownUtils"; @@ -36,34 +37,6 @@ const parseNumberOrThrow = (value: string, label: string): number => { } return parsed; }; - -const parseDateOrThrow = (value: string, label: string): string => { - const [datePart, timePart] = value.split(" "); - const [day, month, year] = datePart.split("/").map(Number); - const [hours, minutes, seconds] = timePart.split(":").map(Number); - const date = new Date(year, month - 1, day, hours, minutes, seconds); - - if (isNaN(date.getTime())) { - throw new Error(`Error with ${label}: ${value}`); - } - const stringDay = String(date.getDate()).padStart(2, "0"); - const stringMonth = String(date.getMonth() + 1).padStart(2, "0"); // Months are zero-based - const stringYear = date.getFullYear(); - const stringHours = String(date.getHours()).padStart(2, "0"); - const stringMinutes = String(date.getMinutes()).padStart(2, "0"); - const stringSeconds = String(date.getSeconds()).padStart(2, "0"); - - return `${stringDay}/${stringMonth}/${stringYear} ${stringHours}:${stringMinutes}:${stringSeconds}`; -}; - -const parseDateOrNull = (value: string): string | undefined => { - const [datePart, timePart] = value.split(" "); - const [day, month, year] = datePart.split("/").map(Number); - const [hours, minutes, seconds] = timePart.split(":").map(Number); - const date = new Date(year, month - 1, day, hours, minutes, seconds); - return isNaN(date.getTime()) ? undefined : date.toISOString(); -}; - const getQuizWithOnlySettings = (settings: string): LocalQuiz => { const name = extractLabelValue(settings, "Name"); @@ -101,10 +74,10 @@ const getQuizWithOnlySettings = (settings: string): LocalQuiz => { ); const rawDueAt = extractLabelValue(settings, "DueAt"); - const dueAt = parseDateOrThrow(rawDueAt, "DueAt"); + const dueAt = timeUtils.parseDateOrThrow(rawDueAt, "DueAt"); const rawLockAt = extractLabelValue(settings, "LockAt"); - const lockAt = parseDateOrNull(rawLockAt); + const lockAt = timeUtils.parseDateOrUndefined(rawLockAt); const description = extractDescription(settings); const localAssignmentGroupName = extractLabelValue( diff --git a/nextjs/src/models/local/timeUtils.ts b/nextjs/src/models/local/timeUtils.ts new file mode 100644 index 0000000..941774a --- /dev/null +++ b/nextjs/src/models/local/timeUtils.ts @@ -0,0 +1,35 @@ +const parseDateOrUndefined = (value: string): string | undefined => { + + // may need to check for other formats + const validDateRegex = /([1-9][1-9]|[0-2])\/(0[1-9]|[1-2][0-9]|3[01])\/\d{4} (0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])/; + if (!validDateRegex.test(value)) { + return undefined; + } + + + const [datePart, timePart] = value.split(" "); + const [day, month, year] = datePart.split("/").map(Number); + const [hours, minutes, seconds] = timePart.split(":").map(Number); + const date = new Date(year, month - 1, day, hours, minutes, seconds); + + if (isNaN(date.getTime())) { + return undefined; + } + const stringDay = String(date.getDate()).padStart(2, "0"); + const stringMonth = String(date.getMonth() + 1).padStart(2, "0"); // Months are zero-based + const stringYear = date.getFullYear(); + const stringHours = String(date.getHours()).padStart(2, "0"); + const stringMinutes = String(date.getMinutes()).padStart(2, "0"); + const stringSeconds = String(date.getSeconds()).padStart(2, "0"); + + return `${stringDay}/${stringMonth}/${stringYear} ${stringHours}:${stringMinutes}:${stringSeconds}`; +}; + +export const timeUtils = { + parseDateOrUndefined, + parseDateOrThrow: (value: string, labelForError: string): string => { + const myDate = parseDateOrUndefined(value); + if (!myDate) throw new Error(`Invalid format for ${labelForError}: ${value}`); + return myDate; + }, +};