From 4aa045d952cfbba3fa01cba84c4d725dfeeab815 Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Thu, 12 Oct 2023 15:08:52 -0600 Subject: [PATCH] working on markdown quiz experience --- Management.Test/Markdown/QuizMarkdownTests.cs | 52 ++++++++ .../Markdown/MarkdownQuestionPreview.razor | 71 +++++----- .../Quiz/Markdown/MarkdownQuizForm.razor | 8 +- .../Quiz/Markdown/QuizPreview.razor | 3 +- Management/Models/Local/LocalQuiz.cs | 10 +- Management/Models/Local/LocalQuizQuestion.cs | 121 +++++++++++++----- 6 files changed, 193 insertions(+), 72 deletions(-) diff --git a/Management.Test/Markdown/QuizMarkdownTests.cs b/Management.Test/Markdown/QuizMarkdownTests.cs index 6f0e309..1abb057 100644 --- a/Management.Test/Markdown/QuizMarkdownTests.cs +++ b/Management.Test/Markdown/QuizMarkdownTests.cs @@ -279,4 +279,56 @@ b) false secondQuestion.Points.Should().Be(2); secondQuestion.QuestionType.Should().Be(QuestionType.MULTIPLE_CHOICE); } + [Test] + public void CanParseEssay() + { + var rawMarkdownQuiz = @" +Name: Test Quiz +LockAtDueDate: true +ShuffleAnswers: true +OneQuestionAtATime: false +DueAt: 2023-08-21T23:59:00 +LockAt: 2023-08-21T23:59:00 +AssignmentGroup: Assignments +AllowedAttempts: -1 +Description: this is the +multi line +description +--- +Which events are triggered when the user clicks on an input field? +essay +"; + + var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz); + var firstQuestion = quiz.Questions.First(); + firstQuestion.Points.Should().Be(1); + firstQuestion.QuestionType.Should().Be(QuestionType.ESSAY); + firstQuestion.Text.Should().NotContain("essay"); + } + [Test] + public void CanParseShortAnswer() + { + var rawMarkdownQuiz = @" +Name: Test Quiz +LockAtDueDate: true +ShuffleAnswers: true +OneQuestionAtATime: false +DueAt: 2023-08-21T23:59:00 +LockAt: 2023-08-21T23:59:00 +AssignmentGroup: Assignments +AllowedAttempts: -1 +Description: this is the +multi line +description +--- +Which events are triggered when the user clicks on an input field? +short answer +"; + + var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz); + var firstQuestion = quiz.Questions.First(); + firstQuestion.Points.Should().Be(1); + firstQuestion.QuestionType.Should().Be(QuestionType.SHORT_ANSWER); + firstQuestion.Text.Should().NotContain("short answer"); + } } \ No newline at end of file diff --git a/Management.Web/Shared/Components/Quiz/Markdown/MarkdownQuestionPreview.razor b/Management.Web/Shared/Components/Quiz/Markdown/MarkdownQuestionPreview.razor index 0e55528..e98a6af 100644 --- a/Management.Web/Shared/Components/Quiz/Markdown/MarkdownQuestionPreview.razor +++ b/Management.Web/Shared/Components/Quiz/Markdown/MarkdownQuestionPreview.razor @@ -6,40 +6,53 @@ } -@((MarkupString)Question.HtmlText) +
+
+ @((MarkupString)Question.HtmlText) +
+
+ @Question.QuestionType +
+
@foreach(var answer in Question.Answers) { + string answerPreview = answer.HtmlText.StartsWith("

") + ? answer.HtmlText.Replace("

", "

") + : answer.HtmlText;

-
- @if(answer.Correct) - { - - - - } - else - { -
- } -
-
- @((MarkupString)answer.HtmlText) + @if(answer.Correct) + { + + + + } + else + { +
+ @if(Question.QuestionType == QuestionType.MULTIPLE_ANSWERS) + { + [ ] + } +
+ } +
+ @((MarkupString)answerPreview)
} \ No newline at end of file diff --git a/Management.Web/Shared/Components/Quiz/Markdown/MarkdownQuizForm.razor b/Management.Web/Shared/Components/Quiz/Markdown/MarkdownQuizForm.razor index ed3e796..2376a05 100644 --- a/Management.Web/Shared/Components/Quiz/Markdown/MarkdownQuizForm.razor +++ b/Management.Web/Shared/Components/Quiz/Markdown/MarkdownQuizForm.razor @@ -23,7 +23,7 @@ error = null; testQuiz = newQuiz; } - catch(Exception e) + catch(QuizMarkdownParseException e) { error = e.Message; } @@ -38,8 +38,6 @@ { if (quizContext.Quiz != null) { - Console.WriteLine("reloading quiz editor"); - if(quizMarkdownInput == "") { quizMarkdownInput = quizContext.Quiz.ToMarkdown(); @@ -66,7 +64,7 @@ private void onHide() { - quizMarkdownInput = ""; + _quizMarkdownInput = ""; quizContext.Quiz = null; } } @@ -95,7 +93,7 @@
@if(error != null) { -

@error

+

@error

}
diff --git a/Management.Web/Shared/Components/Quiz/Markdown/QuizPreview.razor b/Management.Web/Shared/Components/Quiz/Markdown/QuizPreview.razor index 9b41bd8..13cfebf 100644 --- a/Management.Web/Shared/Components/Quiz/Markdown/QuizPreview.razor +++ b/Management.Web/Shared/Components/Quiz/Markdown/QuizPreview.razor @@ -13,7 +13,6 @@ } private void reload() { - Console.WriteLine(JsonSerializer.Serialize(quizContext.Quiz)); this.InvokeAsync(this.StateHasChanged); } public void Dispose() @@ -38,7 +37,7 @@ @foreach(var question in Quiz.Questions) { -
+
!string.IsNullOrWhiteSpace(str)) - .Select(q => LocalQuizQuestion.ParseMarkdown(q)) + .Select((q, i) => LocalQuizQuestion.ParseMarkdown(q, i)) .ToArray(); return quizWithoutQuestions with { @@ -128,3 +128,11 @@ Description: {Description} return string.Empty; } } + +public class QuizMarkdownParseException : Exception +{ + public QuizMarkdownParseException(string message): base(message) + { + + } +} \ No newline at end of file diff --git a/Management/Models/Local/LocalQuizQuestion.cs b/Management/Models/Local/LocalQuizQuestion.cs index 72de689..a13f936 100644 --- a/Management/Models/Local/LocalQuizQuestion.cs +++ b/Management/Models/Local/LocalQuizQuestion.cs @@ -1,3 +1,4 @@ +using System.Security.Cryptography; using System.Text.RegularExpressions; namespace LocalModels; @@ -36,21 +37,47 @@ public record LocalQuizQuestion } private static readonly string[] validFirstAnswerDelimiters = new string[] { "*a)", "a)", "[ ]", "[*]" }; - public static LocalQuizQuestion ParseMarkdown(string input) + + public static LocalQuizQuestion ParseMarkdown(string input, int questionIndex) { - var lines = input.Split(Environment.NewLine); + var lines = input.Trim().Split(Environment.NewLine); var firstLineIsPoints = lines.First().Contains("points: ", StringComparison.CurrentCultureIgnoreCase); - int points = firstLineIsPoints ? int.Parse(lines.First().Split(": ")[1]) : 1; + + var textHasPoints = lines.Length > 0 + && lines.First().Contains(": ") + && lines.First().Split(": ").Length > 1 + && int.TryParse(lines.First().Split(": ")[1], out _); + + int points = firstLineIsPoints && textHasPoints ? int.Parse(lines.First().Split(": ")[1]) : 1; var linesWithoutPoints = firstLineIsPoints ? lines[1..] : lines; var linesWithoutAnswers = linesWithoutPoints - .TakeWhile(line => !validFirstAnswerDelimiters.Any(prefix => line.TrimStart().StartsWith(prefix))) + .TakeWhile( + (line, index) => + !validFirstAnswerDelimiters.Any(prefix => line.TrimStart().StartsWith(prefix)) + ) .ToArray(); - var description = string.Join(Environment.NewLine, linesWithoutAnswers); - var (answers, questionType) = getAnswers(linesWithoutPoints); + var questionType = getQuestionType(linesWithoutPoints, questionIndex); + + var questionTypesWithoutAnswers = new string[] { "essay", "short answer", "short_answer" }; + var descriptionLines = questionTypesWithoutAnswers.Contains(questionType.ToLower()) + ? linesWithoutAnswers + .TakeWhile( + (line, index) => index != linesWithoutPoints.Length && !questionTypesWithoutAnswers.Contains(line.ToLower()) + ) + .ToArray() + : linesWithoutAnswers; + var description = string.Join(Environment.NewLine, descriptionLines); + + + + var typesWithAnswers = new string[] { "multiple_choice", "multiple_answers" }; + var answers = typesWithAnswers.Contains(questionType) + ? getAnswers(linesWithoutPoints, questionIndex) + : []; return new LocalQuizQuestion() { @@ -61,55 +88,79 @@ public record LocalQuizQuestion }; } - private static (LocalQuizQuestionAnswer[], string questionType) getAnswers(string[] linesWithoutPoints) + private static string getQuestionType(string[] linesWithoutPoints, int questionIndex) + { + + if (linesWithoutPoints.Length == 0) + return ""; + if (linesWithoutPoints[^1].Equals("essay", StringComparison.CurrentCultureIgnoreCase)) + return "essay"; + if (linesWithoutPoints[^1].Equals("short answer", StringComparison.CurrentCultureIgnoreCase)) + return "short_answer"; + if (linesWithoutPoints[^1].Equals("short_answer", StringComparison.CurrentCultureIgnoreCase)) + return "short_answer"; + + var answerLines = getAnswersGroupedByLines(linesWithoutPoints, questionIndex); + var isMultipleChoice = + answerLines.First().StartsWith("a)") + || answerLines.First().StartsWith("*a)"); + if (isMultipleChoice) + return "multiple_choice"; + + var isMultipleAnswer = + answerLines.First().StartsWith("[ ]") + || answerLines.First().StartsWith("[*]"); + + if (isMultipleAnswer) + return "multiple_answers"; + + return ""; + } + + private static List getAnswersGroupedByLines(string[] linesWithoutPoints, int questionIndex) { var indexOfAnswerStart = linesWithoutPoints .ToList() .FindIndex( l => validFirstAnswerDelimiters.Any(prefix => l.TrimStart().StartsWith(prefix)) ); + if (indexOfAnswerStart == -1) + { + var debugLine = linesWithoutPoints.FirstOrDefault(l => l.Trim().Length > 0); + throw new QuizMarkdownParseException($"question {questionIndex + 1}: no answers when detecting question type on {debugLine}"); + } + var answerLinesRaw = linesWithoutPoints[indexOfAnswerStart..]; var answerStartPattern = @"^(\*?[a-z]\))|\[\s*\]|\[\*\]"; var answerLines = answerLinesRaw.Aggregate(new List(), (acc, line) => { - if (!Regex.IsMatch(line, answerStartPattern)) + var isNewAnswer = Regex.IsMatch(line, answerStartPattern); + if (isNewAnswer) { - if (acc.Count != 0) // Append to the previous line if there is one - { - int lastIndex = acc.Count - 1; - acc[lastIndex] += Environment.NewLine + line; - } - else - { - acc.Add(line); - } + acc.Add(line); + return acc; } + + if (acc.Count != 0) // Append to the previous line if there is one + acc[^1] += Environment.NewLine + line; else - { - acc.Add(line); // Add as a new line if it matches the pattern - } + acc.Add(line); return acc; }); + return answerLines; + } - var answers = answerLines.Select(LocalQuizQuestionAnswer.ParseMarkdown).ToArray(); + private static LocalQuizQuestionAnswer[] getAnswers(string[] linesWithoutPoints, int questionIndex) + { + var answerLines = getAnswersGroupedByLines(linesWithoutPoints, questionIndex); - var isMultipleChoice = - answerLines.First().StartsWith("a)") - || answerLines.First().StartsWith("*a)"); + var answers = answerLines + .Select((a, i) => LocalQuizQuestionAnswer.ParseMarkdown(a)) + .ToArray(); - var isMultipleAnswer = - answerLines.First().StartsWith("[ ]") - || answerLines.First().StartsWith("[*]"); - - var questionType = isMultipleChoice - ? "multiple_choice" - : isMultipleAnswer - ? "multiple_answers" - : ""; - - return (answers, questionType); + return answers; } }