adding matching suppiort

This commit is contained in:
2023-12-11 17:58:19 -07:00
parent c9f185dea9
commit 95b913ec05
8 changed files with 206 additions and 38 deletions

View File

@@ -314,6 +314,46 @@ short_answer";
}; };
var quizMarkdown = quiz.ToMarkdown(); var quizMarkdown = quiz.ToMarkdown();
var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown);
parsedQuiz.Should().BeEquivalentTo(quiz);
}
[Test]
public void SerializationIsDeterministic_Matching()
{
var quiz = new LocalQuiz()
{
Name = "Test Quiz",
Description = "quiz description",
LockAt = new DateTime(2022, 10, 3, 12, 5, 0),
DueAt = new DateTime(2022, 10, 3, 12, 5, 0),
ShuffleAnswers = true,
OneQuestionAtATime = true,
LocalAssignmentGroupName = "Assignments",
Questions = new LocalQuizQuestion[]
{
new ()
{
Text = "test matching",
QuestionType = QuestionType.MATCHING,
Points = 1,
Answers = new LocalQuizQuestionAnswer[]
{
new() {
Correct = true,
Text="yes",
MatchedText = "testing yes"
},
new() {
Correct = true,
Text="no",
MatchedText = "testing no"
}
}
}
}
};
var quizMarkdown = quiz.ToMarkdown();
var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown); var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown);
parsedQuiz.Should().BeEquivalentTo(quiz); parsedQuiz.Should().BeEquivalentTo(quiz);
} }

View File

@@ -96,6 +96,7 @@ oneline question
"; ";
markdown.Should().Contain(expectedQuestionString); markdown.Should().Contain(expectedQuestionString);
} }
[Test] [Test]
public void CanParseQuestionWithMultipleAnswers() public void CanParseQuestionWithMultipleAnswers()
{ {
@@ -158,6 +159,7 @@ essay
firstQuestion.QuestionType.Should().Be(QuestionType.ESSAY); firstQuestion.QuestionType.Should().Be(QuestionType.ESSAY);
firstQuestion.Text.Should().NotContain("essay"); firstQuestion.Text.Should().NotContain("essay");
} }
[Test] [Test]
public void CanParseShortAnswer() public void CanParseShortAnswer()
{ {
@@ -183,6 +185,7 @@ short answer
firstQuestion.QuestionType.Should().Be(QuestionType.SHORT_ANSWER); firstQuestion.QuestionType.Should().Be(QuestionType.SHORT_ANSWER);
firstQuestion.Text.Should().NotContain("short answer"); firstQuestion.Text.Should().NotContain("short answer");
} }
[Test] [Test]
public void ShortAnswerToMarkdown_IsCorrect() public void ShortAnswerToMarkdown_IsCorrect()
{ {
@@ -239,4 +242,61 @@ Which events are triggered when the user clicks on an input field?
essay"; essay";
questionMarkdown.Should().Contain(expectedMarkdown); questionMarkdown.Should().Contain(expectedMarkdown);
} }
[Test]
public void CanParseMatchingQuestion()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description:
---
Match the following terms & definitions
^ statement - a single command to be executed
^ identifier - name of a variable
^ keyword - reserved word that has special meaning in a program (e.g. class, void, static, etc.)
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var firstQuestion = quiz.Questions.First();
firstQuestion.QuestionType.Should().Be(QuestionType.MATCHING);
firstQuestion.Text.Should().NotContain("statement");
firstQuestion.Answers.First().MatchedText.Should().Be("a single command to be executed");
}
[Test]
public void CanCreateMarkdownForMatchingQuesiton()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description:
---
Match the following terms & definitions
^ statement - a single command to be executed
^ identifier - name of a variable
^ keyword - reserved word that has special meaning in a program (e.g. class, void, static, etc.)
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var questionMarkdown = quiz.Questions.First().ToMarkdown();
var expectedMarkdown = @"Points: 1
Match the following terms & definitions
^ statement - a single command to be executed
^ identifier - name of a variable
^ keyword - reserved word that has special meaning in a program (e.g. class, void, static, etc.)";
questionMarkdown.Should().Contain(expectedMarkdown);
}
} }

View File

@@ -165,7 +165,7 @@
@assignmentContext.Assignment?.Name @assignmentContext.Assignment?.Name
</div> </div>
<section class="flex-1 p-1 border rounded-4 bg-dark-subtle" style="min-height: 0;"> <section class="flex-grow-1 p-1 border rounded-4 bg-dark-subtle" style="min-height: 0;">
@if (assignmentContext.Assignment != null) @if (assignmentContext.Assignment != null)
{ {
<AssignmentMarkdownEditor /> <AssignmentMarkdownEditor />

View File

@@ -13,25 +13,7 @@ public record LocalQuizQuestion
Enumerable.Empty<LocalQuizQuestionAnswer>(); Enumerable.Empty<LocalQuizQuestionAnswer>();
public string ToMarkdown() public string ToMarkdown()
{ {
var answerArray = Answers.Select((answer, i) => var answerArray = Answers.Select(getAnswerMarkdown);
{
var questionLetter = (char)(i + 97);
var isMultipleChoice = QuestionType == "multiple_choice";
var correctIndicator = answer.Correct ? "*" : isMultipleChoice ? "" : " ";
var questionTypeIndicator = isMultipleChoice
? $"{correctIndicator}{questionLetter}) "
: $"[{correctIndicator}] ";
// var textWithSpecificNewline = answer.Text.Replace(Environment.NewLine, Environment.NewLine + " ");
var multilineMarkdownCompatibleText = answer.Text.StartsWith("```")
? Environment.NewLine + answer.Text
: answer.Text;
return $"{questionTypeIndicator}{multilineMarkdownCompatibleText}";
});
var answersText = string.Join(Environment.NewLine, answerArray); var answersText = string.Join(Environment.NewLine, answerArray);
var questionTypeIndicator = QuestionType == "essay" || QuestionType == "short_answer" ? QuestionType : ""; var questionTypeIndicator = QuestionType == "essay" || QuestionType == "short_answer" ? QuestionType : "";
@@ -40,7 +22,34 @@ public record LocalQuizQuestion
{answersText}{questionTypeIndicator}"; {answersText}{questionTypeIndicator}";
} }
private static readonly string[] validFirstAnswerDelimiters = new string[] { "*a)", "a)", "[ ]", "[*]" }; private string getAnswerMarkdown(LocalQuizQuestionAnswer answer, int index)
{
var multilineMarkdownCompatibleText = answer.Text.StartsWith("```")
? Environment.NewLine + answer.Text
: answer.Text;
if (QuestionType == "multiple_answers")
{
var correctIndicator = answer.Correct ? "*" : " ";
var questionTypeIndicator = $"[{correctIndicator}] ";
return $"{questionTypeIndicator}{multilineMarkdownCompatibleText}";
}
else if(QuestionType == "matching")
{
return $"^ {answer.Text} - {answer.MatchedText}";
}
else
{
var questionLetter = (char)(index + 97);
var correctIndicator = answer.Correct ? "*" : "";
var questionTypeIndicator = $"{correctIndicator}{questionLetter}) ";
return $"{questionTypeIndicator}{multilineMarkdownCompatibleText}";
}
}
private static readonly string[] _validFirstAnswerDelimiters = ["*a)", "a)", "[ ]", "[*]", "^"];
public static LocalQuizQuestion ParseMarkdown(string input, int questionIndex) public static LocalQuizQuestion ParseMarkdown(string input, int questionIndex)
{ {
@@ -59,7 +68,7 @@ public record LocalQuizQuestion
var linesWithoutAnswers = linesWithoutPoints var linesWithoutAnswers = linesWithoutPoints
.TakeWhile( .TakeWhile(
(line, index) => (line, index) =>
!validFirstAnswerDelimiters.Any(prefix => line.TrimStart().StartsWith(prefix)) !_validFirstAnswerDelimiters.Any(prefix => line.TrimStart().StartsWith(prefix))
) )
.ToArray(); .ToArray();
@@ -67,6 +76,7 @@ public record LocalQuizQuestion
var questionType = getQuestionType(linesWithoutPoints, questionIndex); var questionType = getQuestionType(linesWithoutPoints, questionIndex);
var questionTypesWithoutAnswers = new string[] { "essay", "short answer", "short_answer" }; var questionTypesWithoutAnswers = new string[] { "essay", "short answer", "short_answer" };
var descriptionLines = questionTypesWithoutAnswers.Contains(questionType.ToLower()) var descriptionLines = questionTypesWithoutAnswers.Contains(questionType.ToLower())
? linesWithoutAnswers ? linesWithoutAnswers
.TakeWhile( .TakeWhile(
@@ -78,9 +88,9 @@ public record LocalQuizQuestion
var typesWithAnswers = new string[] { "multiple_choice", "multiple_answers" }; var typesWithAnswers = new string[] { "multiple_choice", "multiple_answers", "matching" };
var answers = typesWithAnswers.Contains(questionType) var answers = typesWithAnswers.Contains(questionType)
? getAnswers(linesWithoutPoints, questionIndex) ? getAnswers(linesWithoutPoints, questionIndex, questionType)
: []; : [];
return new LocalQuizQuestion() return new LocalQuizQuestion()
@@ -118,6 +128,10 @@ public record LocalQuizQuestion
if (isMultipleAnswer) if (isMultipleAnswer)
return "multiple_answers"; return "multiple_answers";
var isMatching = answerLines.First().StartsWith("^");
if (isMatching)
return "matching";
return ""; return "";
} }
@@ -126,7 +140,7 @@ public record LocalQuizQuestion
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) if (indexOfAnswerStart == -1)
{ {
@@ -136,7 +150,7 @@ public record LocalQuizQuestion
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) =>
{ {
var isNewAnswer = Regex.IsMatch(line, answerStartPattern); var isNewAnswer = Regex.IsMatch(line, answerStartPattern);
@@ -156,12 +170,12 @@ public record LocalQuizQuestion
return answerLines; return answerLines;
} }
private static LocalQuizQuestionAnswer[] getAnswers(string[] linesWithoutPoints, int questionIndex) private static LocalQuizQuestionAnswer[] getAnswers(string[] linesWithoutPoints, int questionIndex, string questionType)
{ {
var answerLines = getAnswersGroupedByLines(linesWithoutPoints, questionIndex); var answerLines = getAnswersGroupedByLines(linesWithoutPoints, questionIndex);
var answers = answerLines var answers = answerLines
.Select((a, i) => LocalQuizQuestionAnswer.ParseMarkdown(a)) .Select((a, i) => LocalQuizQuestionAnswer.ParseMarkdown(a, questionType))
.ToArray(); .ToArray();
return answers; return answers;
@@ -174,6 +188,7 @@ public static class QuestionType
public static readonly string MULTIPLE_CHOICE = "multiple_choice"; public static readonly string MULTIPLE_CHOICE = "multiple_choice";
public static readonly string ESSAY = "essay"; public static readonly string ESSAY = "essay";
public static readonly string SHORT_ANSWER = "short_answer"; public static readonly string SHORT_ANSWER = "short_answer";
public static readonly string MATCHING = "matching";
// possible support for: calculated, file_upload, fill_in_multiple_blanks, matching, multiple_dropdowns, numerical, text_only, true_false, // possible support for: calculated, file_upload, fill_in_multiple_blanks, matching, multiple_dropdowns, numerical, text_only, true_false,
public static readonly IEnumerable<string> AllTypes = new string[] public static readonly IEnumerable<string> AllTypes = new string[]
@@ -182,5 +197,7 @@ public static class QuestionType
MULTIPLE_CHOICE, MULTIPLE_CHOICE,
ESSAY, ESSAY,
SHORT_ANSWER, SHORT_ANSWER,
MATCHING
}; };
} }

View File

@@ -8,14 +8,24 @@ public record LocalQuizQuestionAnswer
public bool Correct { get; init; } public bool Correct { get; init; }
public string Text { get; init; } = string.Empty; public string Text { get; init; } = string.Empty;
public string? MatchedText { get; init; }
public string HtmlText => Markdig.Markdown.ToHtml(Text); public string HtmlText => Markdig.Markdown.ToHtml(Text);
public static LocalQuizQuestionAnswer ParseMarkdown(string input) public static LocalQuizQuestionAnswer ParseMarkdown(string input, string questionType)
{ {
var isCorrect = input[0] == '*' || input[1] == '*'; var isCorrect = input[0] == '*' || input[1] == '*';
string startingQuestionPattern = @"^(\*?[a-z]\))|\[\s*\]|\[\*\] "; string startingQuestionPattern = @"^(\*?[a-z]\))|\[\s*\]|\[\*\]|\^ ";
var text = Regex.Replace(input, startingQuestionPattern, string.Empty).Trim(); var text = Regex.Replace(input, startingQuestionPattern, string.Empty).Trim();
if(questionType == QuestionType.MATCHING)
return new LocalQuizQuestionAnswer()
{
Correct = true,
Text = text.Split('-')[0].Trim(),
MatchedText = string.Join("", text.Split('-')[1..]).Trim(),
};
return new LocalQuizQuestionAnswer() return new LocalQuizQuestionAnswer()
{ {
Correct = isCorrect, Correct = isCorrect,

View File

@@ -1,20 +1,25 @@
#!/bin/bash #!/bin/bash
VERSION="1.1" MAJOR_VERSION="1"
MINOR_VERSION="2"
VERSION="$MAJOR_VERSION.$MINOR_VERSION"
dotnet publish Management.Web/ \ dotnet publish Management.Web/ \
--os linux \ --os linux \
--arch x64 \ --arch x64 \
/t:PublishContainer \ /t:PublishContainer \
-c Release \ -c Release \
-p ContainerImageTags="\"$VERSION;latest\"" \ -p ContainerImageTags="\"$MAJOR_VERSION;$VERSION;latest\"" \
-p ContainerRepository="canvas_management" -p ContainerRepository="canvas_management"
echo "to push run: " echo "to push run: "
echo ""
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:$VERSION" echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:$VERSION"
echo "docker image tag canvas_management:$MAJOR_VERSION alexmickelson/canvas_management:$MAJOR_VERSION"
echo "docker image tag canvas_management:latest alexmickelson/canvas_management:latest" echo "docker image tag canvas_management:latest alexmickelson/canvas_management:latest"
echo "docker push alexmickelson/canvas_management:$VERSION" echo "docker push alexmickelson/canvas_management:$VERSION"
echo "docker push alexmickelson/canvas_management:$MAJOR_VERSION"
echo "docker push alexmickelson/canvas_management:latest" echo "docker push alexmickelson/canvas_management:latest"

View File

@@ -1,6 +1,6 @@
services: services:
canvas_manager: canvas_manager:
image: alexmickelson/canvas_management:1.0 image: alexmickelson/canvas_management:1
user: "1000:1000" user: "1000:1000"
ports: ports:
- 8080:8080 - 8080:8080
@@ -9,6 +9,8 @@ services:
environment: environment:
- storageDirectory=/app/storage - storageDirectory=/app/storage
volumes: volumes:
- ./storage:/app/storage # - ./storage:/app/storage
# - ~/projects/faculty/1410/2023-fall-alex/modules:/app/storage/1410 # - ~/projects/faculty/1410/2023-fall-alex/modules:/app/storage/1410
# - ~/projects/faculty/1810/2024-spring-alex/modules:/app/storage/web_intro - ~/projects/faculty/1810/2024-spring-alex/modules:/app/storage/web_intro
- ~/projects/faculty/1400/2024_spring_alex/modules:/app/storage/1400
- ~/projects/faculty/1405/2024_spring_alex/modules:/app/storage/1405

View File

@@ -1,13 +1,13 @@
GET https://snow.instructure.com/api/v1/courses/871954/quizzes GET https://snow.instructure.com/api/v1/courses/958185/quizzes
Authorization: Bearer {{$dotenv CANVAS_TOKEN}} Authorization: Bearer {{$dotenv CANVAS_TOKEN}}
### ###
GET https://snow.instructure.com/api/v1/courses/871954/assignments GET https://snow.instructure.com/api/v1/courses/958185/assignments
Authorization: Bearer {{$dotenv CANVAS_TOKEN}} Authorization: Bearer {{$dotenv CANVAS_TOKEN}}
### ###
POST https://snow.instructure.com/api/v1/courses/871954/quizzes/3243305/questions POST https://snow.instructure.com/api/v1/courses/958185/quizzes/3358912/questions
Authorization: Bearer {{$dotenv CANVAS_TOKEN}} Authorization: Bearer {{$dotenv CANVAS_TOKEN}}
Content-Type: application/json Content-Type: application/json
@@ -44,3 +44,37 @@ Content-Type: application/json
] ]
} }
} }
###
POST https://snow.instructure.com/api/v1/courses/958185/quizzes/3358912/questions
Authorization: Bearer {{$dotenv CANVAS_TOKEN}}
Content-Type: application/json
{
"question": {
"question_text": "dummy matching via the api",
"question_type": "matching_question",
"points_possible": 5,
"answers": [
{
"correct": true,
"answer_match_left": "statement",
"answer_match_right": "a single command to be executed"
},
{
"correct": true,
"answer_match_left": "identifier",
"answer_match_right": "name of a variable"
},
{
"correct": true,
"answer_match_left": "keyword",
"answer_match_right": "reserved word that has special meaning in a program (e.g. class, void, static, etc.)"
},
{
"correct": true,
"answer_match_left": "source file",
"answer_match_right": "the .cs text file containing your source code"
}
]
}
}