mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
working on markdown quiz experience
This commit is contained in:
@@ -279,4 +279,56 @@ b) false
|
|||||||
secondQuestion.Points.Should().Be(2);
|
secondQuestion.Points.Should().Be(2);
|
||||||
secondQuestion.QuestionType.Should().Be(QuestionType.MULTIPLE_CHOICE);
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,40 +6,53 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@((MarkupString)Question.HtmlText)
|
<div class="row justify-content-between">
|
||||||
|
<div class="col">
|
||||||
|
@((MarkupString)Question.HtmlText)
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
@Question.QuestionType
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@foreach(var answer in Question.Answers)
|
@foreach(var answer in Question.Answers)
|
||||||
{
|
{
|
||||||
|
string answerPreview = answer.HtmlText.StartsWith("<p>")
|
||||||
|
? answer.HtmlText.Replace("<p>", "<p class='m-0'>")
|
||||||
|
: answer.HtmlText;
|
||||||
|
|
||||||
<div class="mx-3 mb-1 bg-dark px-2 rounded rounded-2 d-flex flex-row border">
|
<div class="mx-3 mb-1 bg-dark px-2 rounded rounded-2 d-flex flex-row border">
|
||||||
<div>
|
@if(answer.Correct)
|
||||||
@if(answer.Correct)
|
{
|
||||||
{
|
<svg
|
||||||
<svg
|
style="width: 1em;"
|
||||||
style="width: 1em;"
|
class="me-1 my-auto"
|
||||||
class="me-1"
|
viewBox="0 0 24 24"
|
||||||
viewBox="0 0 24 24"
|
fill="none"
|
||||||
fill="none"
|
>
|
||||||
>
|
<path
|
||||||
<path
|
d="M4 12.6111L8.92308 17.5L20 6.5"
|
||||||
d="M4 12.6111L8.92308 17.5L20 6.5"
|
stroke="var(--bs-success)"
|
||||||
stroke="var(--bs-success)"
|
stroke-width="2"
|
||||||
stroke-width="2"
|
stroke-linecap="round"
|
||||||
stroke-linecap="round"
|
stroke-linejoin="round"
|
||||||
stroke-linejoin="round"
|
/>
|
||||||
/>
|
</svg>
|
||||||
</svg>
|
}
|
||||||
}
|
else
|
||||||
else
|
{
|
||||||
{
|
<div
|
||||||
<div
|
class="me-1 my-auto"
|
||||||
class="me-1"
|
style="width: 1em;"
|
||||||
style="width: 1em;"
|
>
|
||||||
></div>
|
@if(Question.QuestionType == QuestionType.MULTIPLE_ANSWERS)
|
||||||
}
|
{
|
||||||
</div>
|
<span>[ ]</span>
|
||||||
<div>
|
}
|
||||||
@((MarkupString)answer.HtmlText)
|
</div>
|
||||||
|
}
|
||||||
|
<div class="markdownQuizAnswerPreview p-1">
|
||||||
|
@((MarkupString)answerPreview)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
error = null;
|
error = null;
|
||||||
testQuiz = newQuiz;
|
testQuiz = newQuiz;
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(QuizMarkdownParseException e)
|
||||||
{
|
{
|
||||||
error = e.Message;
|
error = e.Message;
|
||||||
}
|
}
|
||||||
@@ -38,8 +38,6 @@
|
|||||||
{
|
{
|
||||||
if (quizContext.Quiz != null)
|
if (quizContext.Quiz != null)
|
||||||
{
|
{
|
||||||
Console.WriteLine("reloading quiz editor");
|
|
||||||
|
|
||||||
if(quizMarkdownInput == "")
|
if(quizMarkdownInput == "")
|
||||||
{
|
{
|
||||||
quizMarkdownInput = quizContext.Quiz.ToMarkdown();
|
quizMarkdownInput = quizContext.Quiz.ToMarkdown();
|
||||||
@@ -66,7 +64,7 @@
|
|||||||
|
|
||||||
private void onHide()
|
private void onHide()
|
||||||
{
|
{
|
||||||
quizMarkdownInput = "";
|
_quizMarkdownInput = "";
|
||||||
quizContext.Quiz = null;
|
quizContext.Quiz = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,7 +93,7 @@
|
|||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
@if(error != null)
|
@if(error != null)
|
||||||
{
|
{
|
||||||
<p class="text-danger">@error</p>
|
<p class="text-danger text-truncate">@error</p>
|
||||||
}
|
}
|
||||||
<QuizPreview Quiz="testQuiz" />
|
<QuizPreview Quiz="testQuiz" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
}
|
}
|
||||||
private void reload()
|
private void reload()
|
||||||
{
|
{
|
||||||
Console.WriteLine(JsonSerializer.Serialize(quizContext.Quiz));
|
|
||||||
this.InvokeAsync(this.StateHasChanged);
|
this.InvokeAsync(this.StateHasChanged);
|
||||||
}
|
}
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -38,7 +37,7 @@
|
|||||||
@foreach(var question in Quiz.Questions)
|
@foreach(var question in Quiz.Questions)
|
||||||
{
|
{
|
||||||
|
|
||||||
<div class="bg-dark-subtle mt-1 p-1 rounded rounded-2">
|
<div class="bg-dark-subtle mt-1 p-1 ps-2 rounded rounded-2">
|
||||||
<MarkdownQuestionPreview
|
<MarkdownQuestionPreview
|
||||||
Question="question"
|
Question="question"
|
||||||
@key="question"
|
@key="question"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ Description: {Description}
|
|||||||
|
|
||||||
var questions = splitInput[1..]
|
var questions = splitInput[1..]
|
||||||
.Where(str => !string.IsNullOrWhiteSpace(str))
|
.Where(str => !string.IsNullOrWhiteSpace(str))
|
||||||
.Select(q => LocalQuizQuestion.ParseMarkdown(q))
|
.Select((q, i) => LocalQuizQuestion.ParseMarkdown(q, i))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
return quizWithoutQuestions with
|
return quizWithoutQuestions with
|
||||||
{
|
{
|
||||||
@@ -128,3 +128,11 @@ Description: {Description}
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class QuizMarkdownParseException : Exception
|
||||||
|
{
|
||||||
|
public QuizMarkdownParseException(string message): base(message)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace LocalModels;
|
namespace LocalModels;
|
||||||
@@ -36,21 +37,47 @@ public record LocalQuizQuestion
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static readonly string[] validFirstAnswerDelimiters = new string[] { "*a)", "a)", "[ ]", "[*]" };
|
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);
|
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 linesWithoutPoints = firstLineIsPoints ? lines[1..] : lines;
|
||||||
|
|
||||||
var linesWithoutAnswers = linesWithoutPoints
|
var linesWithoutAnswers = linesWithoutPoints
|
||||||
.TakeWhile(line => !validFirstAnswerDelimiters.Any(prefix => line.TrimStart().StartsWith(prefix)))
|
.TakeWhile(
|
||||||
|
(line, index) =>
|
||||||
|
!validFirstAnswerDelimiters.Any(prefix => line.TrimStart().StartsWith(prefix))
|
||||||
|
)
|
||||||
.ToArray();
|
.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()
|
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<string> getAnswersGroupedByLines(string[] linesWithoutPoints, int questionIndex)
|
||||||
{
|
{
|
||||||
var indexOfAnswerStart = linesWithoutPoints
|
var indexOfAnswerStart = linesWithoutPoints
|
||||||
.ToList()
|
.ToList()
|
||||||
.FindIndex(
|
.FindIndex(
|
||||||
l => validFirstAnswerDelimiters.Any(prefix => l.TrimStart().StartsWith(prefix))
|
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 answerLinesRaw = linesWithoutPoints[indexOfAnswerStart..];
|
||||||
|
|
||||||
var answerStartPattern = @"^(\*?[a-z]\))|\[\s*\]|\[\*\]";
|
var answerStartPattern = @"^(\*?[a-z]\))|\[\s*\]|\[\*\]";
|
||||||
var answerLines = answerLinesRaw.Aggregate(new List<string>(), (acc, line) =>
|
var answerLines = answerLinesRaw.Aggregate(new List<string>(), (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
|
acc.Add(line);
|
||||||
{
|
return acc;
|
||||||
int lastIndex = acc.Count - 1;
|
|
||||||
acc[lastIndex] += Environment.NewLine + line;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
acc.Add(line);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (acc.Count != 0) // Append to the previous line if there is one
|
||||||
|
acc[^1] += Environment.NewLine + line;
|
||||||
else
|
else
|
||||||
{
|
acc.Add(line);
|
||||||
acc.Add(line); // Add as a new line if it matches the pattern
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
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 =
|
var answers = answerLines
|
||||||
answerLines.First().StartsWith("a)")
|
.Select((a, i) => LocalQuizQuestionAnswer.ParseMarkdown(a))
|
||||||
|| answerLines.First().StartsWith("*a)");
|
.ToArray();
|
||||||
|
|
||||||
var isMultipleAnswer =
|
return answers;
|
||||||
answerLines.First().StartsWith("[ ]")
|
|
||||||
|| answerLines.First().StartsWith("[*]");
|
|
||||||
|
|
||||||
var questionType = isMultipleChoice
|
|
||||||
? "multiple_choice"
|
|
||||||
: isMultipleAnswer
|
|
||||||
? "multiple_answers"
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return (answers, questionType);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user