mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
only working with dates at at string level on the model
This commit is contained in:
@@ -101,7 +101,7 @@ Match the following terms & definitions
|
|||||||
";
|
";
|
||||||
|
|
||||||
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
|
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]
|
[Fact]
|
||||||
public void CanHaveDistractorsAndBePersisted()
|
public void CanHaveDistractorsAndBePersisted()
|
||||||
@@ -118,7 +118,7 @@ Description:
|
|||||||
---
|
---
|
||||||
Match the following terms & definitions
|
Match the following terms & definitions
|
||||||
|
|
||||||
^statement - a single command to be executed
|
^ statement - a single command to be executed
|
||||||
^ - this is the distractor
|
^ - this is the distractor
|
||||||
";
|
";
|
||||||
|
|
||||||
|
|||||||
@@ -195,8 +195,7 @@ public class QuizDeterministicChecks
|
|||||||
Text = "test matching",
|
Text = "test matching",
|
||||||
QuestionType = QuestionType.MATCHING,
|
QuestionType = QuestionType.MATCHING,
|
||||||
Points = 1,
|
Points = 1,
|
||||||
Answers = new LocalQuizQuestionAnswer[]
|
Answers = [
|
||||||
{
|
|
||||||
new() {
|
new() {
|
||||||
Correct = true,
|
Correct = true,
|
||||||
Text="yes",
|
Text="yes",
|
||||||
@@ -207,7 +206,7 @@ public class QuizDeterministicChecks
|
|||||||
Text="no",
|
Text="no",
|
||||||
MatchedText = "testing no"
|
MatchedText = "testing no"
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,10 +13,17 @@ public record LocalQuizQuestion
|
|||||||
public double Points { get; init; }
|
public double Points { get; init; }
|
||||||
public IEnumerable<LocalQuizQuestionAnswer> Answers { get; init; } =
|
public IEnumerable<LocalQuizQuestionAnswer> Answers { get; init; } =
|
||||||
Enumerable.Empty<LocalQuizQuestionAnswer>();
|
Enumerable.Empty<LocalQuizQuestionAnswer>();
|
||||||
|
public IEnumerable<string> MatchDistractors { get; init; } = [];
|
||||||
public string ToMarkdown()
|
public string ToMarkdown()
|
||||||
{
|
{
|
||||||
var answerArray = Answers.Select(getAnswerMarkdown);
|
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 : "";
|
var questionTypeIndicator = QuestionType == "essay" || QuestionType == "short_answer" ? QuestionType : "";
|
||||||
|
|
||||||
return $@"Points: {Points}
|
return $@"Points: {Points}
|
||||||
@@ -39,10 +46,7 @@ public record LocalQuizQuestion
|
|||||||
}
|
}
|
||||||
else if (QuestionType == "matching")
|
else if (QuestionType == "matching")
|
||||||
{
|
{
|
||||||
var distractorText = answer.MatchDistractors?.Select(
|
return $"^ {answer.Text} - {answer.MatchedText}";
|
||||||
d => $"\n^ - {d}"
|
|
||||||
).Join("") ?? "";
|
|
||||||
return $"^ {answer.Text} - {answer.MatchedText}" + distractorText;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -98,12 +102,22 @@ public record LocalQuizQuestion
|
|||||||
? getAnswers(linesWithoutPoints, questionIndex, questionType)
|
? 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()
|
return new LocalQuizQuestion()
|
||||||
{
|
{
|
||||||
Text = description,
|
Text = description,
|
||||||
Points = points,
|
Points = points,
|
||||||
Answers = answers,
|
Answers = answersWithoutDistractors,
|
||||||
QuestionType = questionType
|
QuestionType = questionType,
|
||||||
|
MatchDistractors = distractors
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,27 +198,6 @@ public record LocalQuizQuestion
|
|||||||
|
|
||||||
var answers = answerLines
|
var answers = answerLines
|
||||||
.Select((a, i) => LocalQuizQuestionAnswer.ParseMarkdown(a, questionType))
|
.Select((a, i) => LocalQuizQuestionAnswer.ParseMarkdown(a, questionType))
|
||||||
.Aggregate([], (IEnumerable<LocalQuizQuestionAnswer> 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();
|
.ToArray();
|
||||||
|
|
||||||
return answers;
|
return answers;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ public record LocalQuizQuestionAnswer
|
|||||||
public string Text { get; init; } = string.Empty;
|
public string Text { get; init; } = string.Empty;
|
||||||
|
|
||||||
public string? MatchedText { get; init; }
|
public string? MatchedText { get; init; }
|
||||||
public IEnumerable<string>? MatchDistractors { get; init; }
|
|
||||||
|
|
||||||
public string HtmlText => MarkdownService.Render(Text);
|
public string HtmlText => MarkdownService.Render(Text);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using CanvasModel.Quizzes;
|
using CanvasModel.Quizzes;
|
||||||
|
|
||||||
using LocalModels;
|
using LocalModels;
|
||||||
|
|
||||||
using RestSharp;
|
using RestSharp;
|
||||||
|
|
||||||
namespace Management.Services.Canvas;
|
namespace Management.Services.Canvas;
|
||||||
@@ -16,7 +18,7 @@ public class CanvasQuizService(
|
|||||||
CanvasServiceUtils utils,
|
CanvasServiceUtils utils,
|
||||||
ICanvasAssignmentService assignments,
|
ICanvasAssignmentService assignments,
|
||||||
ILogger<CanvasQuizService> logger
|
ILogger<CanvasQuizService> logger
|
||||||
): ICanvasQuizService
|
) : ICanvasQuizService
|
||||||
{
|
{
|
||||||
private readonly IWebRequestor webRequestor = webRequestor;
|
private readonly IWebRequestor webRequestor = webRequestor;
|
||||||
private readonly CanvasServiceUtils utils = utils;
|
private readonly CanvasServiceUtils utils = utils;
|
||||||
@@ -164,6 +166,7 @@ public class CanvasQuizService(
|
|||||||
|
|
||||||
var url = $"courses/{canvasCourseId}/quizzes/{canvasQuizId}/questions";
|
var url = $"courses/{canvasCourseId}/quizzes/{canvasQuizId}/questions";
|
||||||
var answers = getAnswers(q);
|
var answers = getAnswers(q);
|
||||||
|
|
||||||
var body = new
|
var body = new
|
||||||
{
|
{
|
||||||
question = new
|
question = new
|
||||||
@@ -172,9 +175,12 @@ public class CanvasQuizService(
|
|||||||
question_type = q.QuestionType + "_question",
|
question_type = q.QuestionType + "_question",
|
||||||
points_possible = q.Points,
|
points_possible = q.Points,
|
||||||
position,
|
position,
|
||||||
|
matching_answer_incorrect_matches = string.Join("\n", q.MatchDistractors),
|
||||||
answers
|
answers
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Console.WriteLine(JsonSerializer.Serialize(q));
|
||||||
|
Console.WriteLine(JsonSerializer.Serialize(body));
|
||||||
var request = new RestRequest(url);
|
var request = new RestRequest(url);
|
||||||
request.AddBody(body);
|
request.AddBody(body);
|
||||||
|
|
||||||
@@ -197,7 +203,6 @@ public class CanvasQuizService(
|
|||||||
{
|
{
|
||||||
answer_match_left = a.Text,
|
answer_match_left = a.Text,
|
||||||
answer_match_right = a.MatchedText,
|
answer_match_right = a.MatchedText,
|
||||||
matching_answer_incorrect_matches = a.MatchDistractors,
|
|
||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { RubricItem } from "./rubricItem";
|
|||||||
export interface LocalAssignment {
|
export interface LocalAssignment {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
lockAt?: string; // ISO 8601 date string
|
lockAt?: string; // 21/08/2023 23:59:00
|
||||||
dueAt: string; // ISO 8601 date string
|
dueAt: string; // 21/08/2023 23:59:00
|
||||||
localAssignmentGroupName?: string;
|
localAssignmentGroupName?: string;
|
||||||
submissionTypes: AssignmentSubmissionType[];
|
submissionTypes: AssignmentSubmissionType[];
|
||||||
allowedFileUploadExtensions: string[];
|
allowedFileUploadExtensions: string[];
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { timeUtils } from "../../timeUtils";
|
||||||
import { AssignmentSubmissionType } from "../assignmentSubmissionType";
|
import { AssignmentSubmissionType } from "../assignmentSubmissionType";
|
||||||
import { LocalAssignment } from "../localAssignment";
|
import { LocalAssignment } from "../localAssignment";
|
||||||
import { RubricItem } from "../rubricItem";
|
import { RubricItem } from "../rubricItem";
|
||||||
@@ -50,12 +51,8 @@ const parseSettings = (input: string) => {
|
|||||||
const submissionTypes = parseSubmissionTypes(input);
|
const submissionTypes = parseSubmissionTypes(input);
|
||||||
const fileUploadExtensions = parseFileUploadExtensions(input);
|
const fileUploadExtensions = parseFileUploadExtensions(input);
|
||||||
|
|
||||||
const lockAt = (rawLockAt ? new Date(rawLockAt) : undefined)?.toISOString();
|
const dueAt = timeUtils.parseDateOrThrow(rawDueAt, "DueAt");
|
||||||
const dueAt = new Date(rawDueAt).toISOString();
|
const lockAt = timeUtils.parseDateOrUndefined(rawLockAt);
|
||||||
|
|
||||||
if (isNaN(new Date(dueAt).getTime())) {
|
|
||||||
throw new Error(`Error with DueAt: ${rawDueAt}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { timeUtils } from "../../timeUtils";
|
||||||
import { LocalQuiz } from "../localQuiz";
|
import { LocalQuiz } from "../localQuiz";
|
||||||
import { quizQuestionMarkdownUtils } from "./quizQuestionMarkdownUtils";
|
import { quizQuestionMarkdownUtils } from "./quizQuestionMarkdownUtils";
|
||||||
|
|
||||||
@@ -36,34 +37,6 @@ const parseNumberOrThrow = (value: string, label: string): number => {
|
|||||||
}
|
}
|
||||||
return parsed;
|
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 getQuizWithOnlySettings = (settings: string): LocalQuiz => {
|
||||||
const name = extractLabelValue(settings, "Name");
|
const name = extractLabelValue(settings, "Name");
|
||||||
|
|
||||||
@@ -101,10 +74,10 @@ const getQuizWithOnlySettings = (settings: string): LocalQuiz => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const rawDueAt = extractLabelValue(settings, "DueAt");
|
const rawDueAt = extractLabelValue(settings, "DueAt");
|
||||||
const dueAt = parseDateOrThrow(rawDueAt, "DueAt");
|
const dueAt = timeUtils.parseDateOrThrow(rawDueAt, "DueAt");
|
||||||
|
|
||||||
const rawLockAt = extractLabelValue(settings, "LockAt");
|
const rawLockAt = extractLabelValue(settings, "LockAt");
|
||||||
const lockAt = parseDateOrNull(rawLockAt);
|
const lockAt = timeUtils.parseDateOrUndefined(rawLockAt);
|
||||||
|
|
||||||
const description = extractDescription(settings);
|
const description = extractDescription(settings);
|
||||||
const localAssignmentGroupName = extractLabelValue(
|
const localAssignmentGroupName = extractLabelValue(
|
||||||
|
|||||||
35
nextjs/src/models/local/timeUtils.ts
Normal file
35
nextjs/src/models/local/timeUtils.ts
Normal file
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user