better matching

This commit is contained in:
2024-08-26 12:52:55 -06:00
parent 884e465df6
commit cafe04faf6
15 changed files with 232 additions and 33 deletions

View File

@@ -127,4 +127,33 @@ Match the following terms & definitions
quizMarkdown.Should().Contain("^ statement - a single command to be executed\n^ - this is the distractor"); quizMarkdown.Should().Contain("^ statement - a single command to be executed\n^ - this is the distractor");
} }
[Fact]
public void DistractorsDoNotAddDelimiterOntheEnd()
{
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:
---
Points: 2
Match up the term with the best possible answer.
^ - a variable name
^ - A reserved word with special meaning to the compiler
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var quizMarkdown = quiz.ToMarkdown();
quizMarkdown.Should().Contain(@"Match up the term with the best possible answer.
^ - a variable name
^ - A reserved word with special meaning to the compiler");
}
} }

View File

@@ -19,13 +19,21 @@ public record LocalQuizQuestionAnswer
if (questionType == QuestionType.MATCHING) if (questionType == QuestionType.MATCHING)
{ {
string matchingPattern = @"^\^ ?"; string matchingPattern = @"^\^";
var textWithoutMatchDelimiter = Regex.Replace(input, matchingPattern, string.Empty).Trim(); var textWithoutMatchDelimiter = Regex.Replace(input, matchingPattern, string.Empty);
var leftRightDelimiter = " - ";
return new LocalQuizQuestionAnswer() return new LocalQuizQuestionAnswer()
{ {
Correct = true, Correct = true,
Text = textWithoutMatchDelimiter.Split('-')[0].Trim(), Text = textWithoutMatchDelimiter.Split(leftRightDelimiter)[0].Trim(),
MatchedText = string.Join("-", textWithoutMatchDelimiter.Split('-')[1..]).Trim(), MatchedText = string.Join(
leftRightDelimiter,
textWithoutMatchDelimiter
.Split(leftRightDelimiter)[1..]
.Select(a => a.Trim())
.Where(a => a != "")
).Trim(),
}; };
} }

View File

@@ -9,9 +9,11 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.52.0", "@tanstack/react-query": "^5.52.0",
"axios": "^1.7.5",
"next": "14.2.5", "next": "14.2.5",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-error-boundary": "^4.0.13",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"yaml": "^2.5.0" "yaml": "^2.5.0"
}, },
@@ -406,7 +408,6 @@
"version": "7.25.0", "version": "7.25.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
"integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
"dev": true,
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@@ -2137,8 +2138,7 @@
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
"dev": true
}, },
"node_modules/available-typed-arrays": { "node_modules/available-typed-arrays": {
"version": "1.0.7", "version": "1.0.7",
@@ -2164,6 +2164,16 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/axios": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz",
"integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": { "node_modules/axobject-query": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
@@ -2425,7 +2435,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": { "dependencies": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
}, },
@@ -2681,7 +2690,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
} }
@@ -3576,6 +3584,25 @@
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true "dev": true
}, },
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -3605,7 +3632,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
@@ -4816,7 +4842,6 @@
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -4825,7 +4850,6 @@
"version": "2.1.35", "version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": { "dependencies": {
"mime-db": "1.52.0" "mime-db": "1.52.0"
}, },
@@ -5594,6 +5618,11 @@
"react-is": "^16.13.1" "react-is": "^16.13.1"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/psl": { "node_modules/psl": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@@ -5658,6 +5687,17 @@
"react": "^18.3.1" "react": "^18.3.1"
} }
}, },
"node_modules/react-error-boundary": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz",
"integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"peerDependencies": {
"react": ">=16.13.1"
}
},
"node_modules/react-hot-toast": { "node_modules/react-hot-toast": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
@@ -5733,8 +5773,7 @@
"node_modules/regenerator-runtime": { "node_modules/regenerator-runtime": {
"version": "0.14.1", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
"dev": true
}, },
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.2", "version": "1.5.2",

View File

@@ -11,9 +11,11 @@
}, },
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.52.0", "@tanstack/react-query": "^5.52.0",
"axios": "^1.7.5",
"next": "14.2.5", "next": "14.2.5",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-error-boundary": "^4.0.13",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"yaml": "^2.5.0" "yaml": "^2.5.0"
}, },

View File

@@ -0,0 +1,3 @@
export async function GET() {
return Response.json([]);
}

View File

@@ -1,9 +1,11 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { createQueryClient } from "@/services/utils/queryClient";
import { dehydrate } from "@tanstack/react-query"; import { dehydrate } from "@tanstack/react-query";
import { MyQueryClientProvider } from "@/services/utils/MyQueryClientProvider"; import { MyQueryClientProvider } from "@/services/utils/MyQueryClientProvider";
import { hydrateCourses } from "@/hooks/hookHydration";
import { LoadingAndErrorHandling } from "@/components/LoadingAndErrorHandling";
import { createQueryClientForServer } from "@/services/utils/queryClientServer";
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
@@ -12,9 +14,9 @@ export const metadata: Metadata = {
}; };
export async function getDehydratedClient() { export async function getDehydratedClient() {
const queryClient = createQueryClient(); const queryClient = createQueryClientForServer();
// await hydrateOpenSections(queryClient); await hydrateCourses(queryClient);
const dehydratedState = dehydrate(queryClient); const dehydratedState = dehydrate(queryClient);
return dehydratedState; return dehydratedState;
} }
@@ -25,10 +27,13 @@ export default async function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const dehydratedState = await getDehydratedClient(); const dehydratedState = await getDehydratedClient();
return ( return (
<html lang="en"> <html lang="en">
<MyQueryClientProvider dehydratedState={dehydratedState}> <MyQueryClientProvider dehydratedState={dehydratedState}>
<LoadingAndErrorHandling>
<body className={inter.className}>{children}</body> <body className={inter.className}>{children}</body>
</LoadingAndErrorHandling>
</MyQueryClientProvider> </MyQueryClientProvider>
</html> </html>
); );

View File

@@ -1,12 +1,12 @@
import { canvasAssignmentService } from "@/services/canvas/canvasAssignmentService"; "use client"
import { useLocalCoursesQuery } from "@/hooks/localCoursesHooks";
export default async function Home() {
const assignments = await canvasAssignmentService.getAll(960410);
export default function Home() {
const { data: courses } = useLocalCoursesQuery();
return ( return (
<main className="flex min-h-screen flex-col items-center justify-between p-24"> <main className="flex min-h-screen flex-col items-center justify-between p-24">
{assignments.map((assignment) => ( {courses.map((c) => (
<div key={assignment.id}>{assignment.name}</div> <div key={c.settings.name}>{c.settings.name} </div>
))} ))}
</main> </main>
); );

View File

@@ -0,0 +1,30 @@
import { QueryErrorResetBoundary } from "@tanstack/react-query";
import { FC, ReactNode, Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
export const LoadingAndErrorHandling: FC<{ children: ReactNode }> = ({
children,
}) => {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={(props) => (
<div className="text-center">
<div className="p-3">{JSON.stringify(props.error)}</div>
<button
className="btn btn-outline-secondary"
onClick={() => props.resetErrorBoundary()}
>
Try again
</button>
</div>
)}
>
<Suspense fallback={<div>loading...</div>}>{children}</Suspense>
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
};

View File

@@ -0,0 +1,10 @@
import { QueryClient } from "@tanstack/react-query";
import { localCourseKeys } from "./localCoursesHooks";
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
export const hydrateCourses = async (queryClient: QueryClient) => {
await queryClient.prefetchQuery({
queryKey: localCourseKeys.allCourses,
queryFn: async () => await fileStorageService.loadSavedCourses(),
});
};

View File

@@ -0,0 +1,17 @@
import { LocalCourse } from "@/models/local/localCourse";
import { useSuspenseQuery } from "@tanstack/react-query";
import axios from "axios";
export const localCourseKeys = {
allCourses: ["all courses"] as const,
};
export const useLocalCoursesQuery = () =>
useSuspenseQuery({
queryKey: localCourseKeys.allCourses,
queryFn: async (): Promise<LocalCourse[]> => {
const url = `/api/courses`;
const response = await axios.get(url);
return response.data;
},
});

View File

@@ -0,0 +1,19 @@
import { describe, it, expect } from "vitest";
import { getDateFromString } from "../timeUtils";
describe("Can properly handle expected date formats", () => {
it("can use AM/PM dates", () =>{
const dateString = "8/27/2024 1:00:00AM"
const dateObject = getDateFromString(dateString)
expect(dateObject).not.toBeUndefined()
})
it("can use 24 hour dates", () =>{
const dateString = "8/27/2024 23:95:00"
const dateObject = getDateFromString(dateString)
expect(dateObject).not.toBeUndefined()
})
})

View File

@@ -1,24 +1,39 @@
export const getDateFromString = (value: string) => { export const getDateFromString = (value: string) => {
// may need to check for other formats // Updated regex to match both formats: "MM/DD/YYYY HH:mm:ss" and "M/D/YYYY h:mm:ss AM/PM"
const validDateRegex = const validDateRegex = /^\d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{2}:\d{2}(?:\s?[APap][Mm])?$/;
/\d{2}\/\d{2}\/\d{4} [0-2][0-9]:[0-5][0-9]:[0-5][0-9]/;
if (!validDateRegex.test(value)) { if (!validDateRegex.test(value)) {
console.log("invalid date format", value);
return undefined; return undefined;
} }
const [datePart, timePart] = value.split(" "); const [datePart, timePartWithMeridian] = value.split(" ");
const [day, month, year] = datePart.split("/").map(Number); const [day, month, year] = datePart.split("/").map(Number);
let [timePart, meridian] = timePartWithMeridian.split(" ");
const [hours, minutes, seconds] = timePart.split(":").map(Number); const [hours, minutes, seconds] = timePart.split(":").map(Number);
const date = new Date(year, month - 1, day, hours, minutes, seconds);
let adjustedHours = hours;
if (meridian) {
meridian = meridian.toUpperCase();
if (meridian === "PM" && hours < 12) {
adjustedHours += 12;
} else if (meridian === "AM" && hours === 12) {
adjustedHours = 0;
}
}
const date = new Date(year, month - 1, day, adjustedHours, minutes, seconds);
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {
console.log("could not parse time out of value", value);
return undefined; return undefined;
} }
return date; return date;
}; };
export const verifyDateStringOrUndefined = ( export const verifyDateStringOrUndefined = (
value: string value: string
): string | undefined => { ): string | undefined => {

View File

@@ -5,6 +5,7 @@ import { courseMarkdownLoader } from "./utils/couresMarkdownLoader";
import { courseMarkdownSaver } from "./utils/courseMarkdownSaver"; import { courseMarkdownSaver } from "./utils/courseMarkdownSaver";
const basePath = process.env.STORAGE_DIRECTORY ?? "./storage"; const basePath = process.env.STORAGE_DIRECTORY ?? "./storage";
console.log("base path", basePath);
export const fileStorageService = { export const fileStorageService = {
async saveCourseAsync( async saveCourseAsync(

View File

@@ -3,6 +3,7 @@
import { import {
DehydratedState, DehydratedState,
hydrate, hydrate,
HydrationBoundary,
QueryClientProvider, QueryClientProvider,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import React from "react"; import React from "react";
@@ -15,9 +16,9 @@ export const MyQueryClientProvider: FC<{
}> = ({ children, dehydratedState }) => { }> = ({ children, dehydratedState }) => {
const [queryClient] = useState(createQueryClient()); const [queryClient] = useState(createQueryClient());
hydrate(queryClient, dehydratedState);
return ( return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider> <QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydratedState}>{children}</HydrationBoundary>
</QueryClientProvider>
); );
}; };

View File

@@ -0,0 +1,20 @@
import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query";
export const createQueryClientForServer = () => new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 0,
},
mutations: {
onError: (e) => console.log(e),
retry: 0,
},
},
queryCache: new QueryCache({
onError: (e) => console.log(e),
}),
mutationCache: new MutationCache({
onError: (e) => console.log(e),
}),
});