lots of shit
This commit is contained in:
parent
5c1f93db0c
commit
dded1c1c2d
22 changed files with 258 additions and 135 deletions
|
@ -1,2 +1,4 @@
|
|||
material
|
||||
===
|
||||
|
||||
Exercises are decoupled from the actual material and can appear wherever they want
|
||||
|
|
|
@ -18,6 +18,9 @@ exercises:
|
|||
description: |
|
||||
Write a function called `doubleIt` that takes an integer and doubles it.
|
||||
|
||||
satisfiesConcept:
|
||||
- fp-function
|
||||
|
||||
graders:
|
||||
ocaml:
|
||||
style: studentModule
|
||||
|
@ -35,6 +38,9 @@ exercises:
|
|||
description: |
|
||||
Which of the following can be described as a _function_?
|
||||
|
||||
concepts:
|
||||
- fp-function
|
||||
|
||||
graders:
|
||||
ocaml:
|
||||
style: multipleChoice
|
||||
|
|
1
materialdb/.gitignore
vendored
1
materialdb/.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
/test.db
|
||||
/dist
|
||||
|
|
3
materialdb/db/index.ts
Normal file
3
materialdb/db/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { init, Page, Exercise, Grader } from "./page";
|
||||
|
||||
export { init, Page, Exercise, Grader };
|
|
@ -3,41 +3,52 @@ import { PrimaryKey, ForeignKey, HasMany, Sequelize, Column, Table, Model, DataT
|
|||
@Table
|
||||
export class Page extends Model {
|
||||
@PrimaryKey
|
||||
@Column
|
||||
@Column(DataType.STRING)
|
||||
public slug: string;
|
||||
|
||||
@Column
|
||||
@Column(DataType.STRING)
|
||||
public title: string;
|
||||
|
||||
@Column
|
||||
@Column(DataType.STRING)
|
||||
public type: string;
|
||||
|
||||
@Column
|
||||
@Column(DataType.STRING)
|
||||
public content: string;
|
||||
}
|
||||
|
||||
@Table
|
||||
export class Exercise extends Model {
|
||||
@PrimaryKey
|
||||
@Column
|
||||
@Column(DataType.STRING)
|
||||
public name: string;
|
||||
|
||||
@HasMany(() => Grader)
|
||||
public graders: Grader[];
|
||||
}
|
||||
|
||||
@Table
|
||||
export class ExerciseSatisfiesConcept extends Model {
|
||||
@ForeignKey(() => Exercise)
|
||||
@Column(DataType.STRING)
|
||||
public exercise_name: string;
|
||||
|
||||
@ForeignKey(() => Page)
|
||||
@Column(DataType.STRING)
|
||||
public concept_slug: string;
|
||||
}
|
||||
|
||||
@Table
|
||||
export class Grader extends Model {
|
||||
@PrimaryKey
|
||||
@ForeignKey(() => Exercise)
|
||||
@Column
|
||||
@Column(DataType.STRING)
|
||||
public exercise_name: string;
|
||||
|
||||
@PrimaryKey
|
||||
@Column
|
||||
@Column(DataType.STRING)
|
||||
public language: string;
|
||||
|
||||
@Column
|
||||
@Column(DataType.STRING)
|
||||
public style: string;
|
||||
|
||||
@Column(DataType.JSON)
|
||||
|
|
|
@ -4,7 +4,7 @@ import * as yaml from "js-yaml";
|
|||
import { plainToClass } from "class-transformer";
|
||||
import { validate } from "class-validator";
|
||||
|
||||
import { init, Page, Exercise, Grader } from "./db/page";
|
||||
import { init, Page, Exercise, Grader } from "./db";
|
||||
import { Page as PageConfig, Exercise as ExerciseConfig, Grader as GraderConfig } from "./page";
|
||||
|
||||
// TODO: configure this thru cmdline or something later
|
||||
|
@ -45,6 +45,9 @@ async function loadPageIntoDb(name: string): Promise<void> {
|
|||
});
|
||||
await exercise.save();
|
||||
|
||||
async function loadConceptsIntoDb(concept: string): Promise<void> {
|
||||
};
|
||||
|
||||
async function loadGraderIntoDb([language, grader_cfg]: [string, GraderConfig]): Promise<void> {
|
||||
let grader = new Grader({
|
||||
page_slug: slug,
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"name": "materialdb",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"start": "ts-node index.ts"
|
||||
"start": "ts-node index.ts",
|
||||
"gen": "tsc --declaration"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.3",
|
||||
|
@ -10,6 +12,7 @@
|
|||
"ts-node": "^10.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"typescript": "^4.4.2",
|
||||
"class-transformer": "^0.4.0",
|
||||
"class-validator": "^0.13.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true
|
||||
"experimentalDecorators": true,
|
||||
"declaration": true,
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"devDependencies": {
|
||||
"@sveltejs/kit": "next",
|
||||
"@types/node": "^16.7.5",
|
||||
"@types/sequelize": "^4.28.10",
|
||||
"@types/validator": "^13.6.3",
|
||||
"@typescript-eslint/eslint-plugin": "^4.19.0",
|
||||
"@typescript-eslint/parser": "^4.19.0",
|
||||
|
@ -31,6 +32,8 @@
|
|||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@types/materialdb": "file:../materialdb",
|
||||
"materialdb": "file:../materialdb",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sequelize": "^6.6.5",
|
||||
"sequelize-typescript": "^2.1.0"
|
||||
|
|
1
web/src/global.d.ts
vendored
1
web/src/global.d.ts
vendored
|
@ -1 +1,2 @@
|
|||
/// <reference types="@sveltejs/kit" />
|
||||
/// <reference types="materialdb" />
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { checkLogin } from "$lib/auth";
|
||||
import { db } from "$lib/db";
|
||||
import { dbPromise } from "$lib/db";
|
||||
|
||||
export async function handle({ request, resolve }) {
|
||||
request.locals.db = await dbPromise;
|
||||
request.locals.loginStatus = checkLogin(request);
|
||||
request.locals.db = await db;
|
||||
return resolve(request);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import MultipleChoice from "$lib/activity/MultipleChoice.svelte";
|
||||
import MultipleChoice from "$lib/exercise/MultipleChoice.svelte";
|
||||
</script>
|
||||
|
||||
<MultipleChoice />
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let question = {
|
||||
|
||||
description: `
|
||||
what is 1 + 1?
|
||||
`,
|
||||
concepts: [ "addition" ],
|
||||
choices: [
|
||||
{ text: "1", correct: false },
|
||||
{ text: "2", correct: true },
|
||||
{ text: "3", correct: false },
|
||||
],
|
||||
};
|
||||
|
||||
// State related variables
|
||||
let state = "ask";
|
||||
let currentChoice: number;
|
||||
let wasCorrect = false;
|
||||
|
||||
let choose = (index: number) => {
|
||||
currentChoice = index;
|
||||
};
|
||||
|
||||
let answer = async (sure: boolean) => {
|
||||
state = "submitting";
|
||||
let resp = await fetch("/api/submit", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
}),
|
||||
});
|
||||
state = "answer";
|
||||
wasCorrect = question.choices[currentChoice].correct;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="quiz-box">
|
||||
{#if state == "ask" }
|
||||
<small>Q:</small>
|
||||
<p>{ question.description }</p>
|
||||
<ul class="choices">
|
||||
{#each question.choices as choice, index }
|
||||
<li>
|
||||
<input type="radio" name="choice" on:change={() => choose(index)} />
|
||||
{ choice.text }
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<div class="answer-buttons">
|
||||
<button>Skip</button>
|
||||
<button on:click={() => answer(false)}>Not sure</button>
|
||||
<button on:click={() => answer(true)}>I'm sure</button>
|
||||
</div>
|
||||
{:else if state == "submitting" }
|
||||
submitting...
|
||||
{:else}
|
||||
<small>A:</small>
|
||||
{#if wasCorrect }
|
||||
<p>you Got it!</p>
|
||||
{:else}
|
||||
<p>you Failed</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.quiz-box {
|
||||
border: 1px solid gray;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
|
||||
.choices {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
flex-grow: 1;
|
||||
margin: 12px;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.answer-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
button {
|
||||
margin: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
10
web/src/lib/auth.ts
Normal file
10
web/src/lib/auth.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { User } from "$lib/db";
|
||||
|
||||
export class LoginStatus {
|
||||
constructor(public isLoggedIn: boolean, public username?: string) {}
|
||||
}
|
||||
|
||||
export function checkLogin(req: Request): LoginStatus {
|
||||
console.log("req", req);
|
||||
return new LoginStatus(false);
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
export function checkLogin(request) {
|
||||
|
||||
}
|
||||
|
||||
export function requireLogin() {
|
||||
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
console.log("second(): called");
|
||||
};
|
||||
}
|
1
web/src/lib/consts.ts
Normal file
1
web/src/lib/consts.ts
Normal file
|
@ -0,0 +1 @@
|
|||
|
|
@ -1,24 +1,27 @@
|
|||
import { Sequelize, DataType, Unique, Column, Table, Model } from "sequelize-typescript";
|
||||
import { Sequelize } from "sequelize-typescript";
|
||||
import { User } from "./user";
|
||||
|
||||
@Table
|
||||
export class User extends Model {
|
||||
@Unique
|
||||
@Column(DataType.STRING)
|
||||
public email: string;
|
||||
import { Page, Exercise, Grader } from "materialdb/db";
|
||||
|
||||
async function loadMaterialDb() {
|
||||
// TODO: ensure this database is read-only?
|
||||
// possibly could just chmod -w it at the start but that's kinda hacky
|
||||
let sequelize = new Sequelize(`sqlite:../materialdb/test.db`, {
|
||||
models: [Page, Exercise, Grader],
|
||||
});
|
||||
await sequelize.sync();
|
||||
return sequelize;
|
||||
}
|
||||
|
||||
async function loadMaterialDb(path?: string) {
|
||||
}
|
||||
|
||||
async function init(path: string): Promise<Sequelize> {
|
||||
console.log("META", import.meta.env);
|
||||
|
||||
let sequelize = new Sequelize(`sqlite:${path}`, {
|
||||
async function init(): Promise<Sequelize> {
|
||||
let sequelize = new Sequelize(`sqlite:test.db`, {
|
||||
models: [User],
|
||||
});
|
||||
await sequelize.sync({ force: true });
|
||||
return sequelize;
|
||||
}
|
||||
|
||||
export let db = init("test.db");
|
||||
export let dbPromise = init();
|
||||
export let materialDb = loadMaterialDb();
|
||||
|
||||
export { User };
|
||||
|
|
16
web/src/lib/db/user.ts
Normal file
16
web/src/lib/db/user.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { DataType, Unique, Column, Table, Model } from "sequelize-typescript";
|
||||
|
||||
@Table
|
||||
export class User extends Model {
|
||||
@Column(DataType.STRING)
|
||||
public email: string;
|
||||
|
||||
@Unique
|
||||
@Column(DataType.STRING)
|
||||
public normalizedEmail: string;
|
||||
}
|
||||
|
||||
@Table
|
||||
export class UserMastery extends Model {
|
||||
|
||||
}
|
141
web/src/lib/exercise/MultipleChoice.svelte
Normal file
141
web/src/lib/exercise/MultipleChoice.svelte
Normal file
|
@ -0,0 +1,141 @@
|
|||
<script lang="ts">
|
||||
export let question = {
|
||||
|
||||
description: `
|
||||
what is 1 + 1?
|
||||
`,
|
||||
concepts: [ "addition" ],
|
||||
choices: [
|
||||
{ text: "1", correct: false },
|
||||
{ text: "2", correct: true },
|
||||
{ text: "3", correct: false },
|
||||
],
|
||||
};
|
||||
|
||||
// State related variables
|
||||
let state = "ask";
|
||||
let currentChoice: number = -1;
|
||||
let wasCorrect = false;
|
||||
|
||||
let choose = (index: number) => {
|
||||
currentChoice = index;
|
||||
};
|
||||
|
||||
let answer = async (sure: boolean) => {
|
||||
state = "submitting";
|
||||
let resp = await fetch("/api/submit", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
}),
|
||||
});
|
||||
state = "answer";
|
||||
wasCorrect = question.choices[currentChoice].correct;
|
||||
};
|
||||
|
||||
let clearChoice = () => {
|
||||
currentChoice = -1;
|
||||
};
|
||||
|
||||
let goBack = () => {
|
||||
state = "ask";
|
||||
currentChoice = -1;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="quiz-box">
|
||||
{#if state == "ask" }
|
||||
<small>Q:</small>
|
||||
{:else}
|
||||
<small>A:</small>
|
||||
{/if}
|
||||
|
||||
<p>{ question.description }</p>
|
||||
<ul class="choices">
|
||||
{#each question.choices as choice, index }
|
||||
<li>
|
||||
<input type="radio"
|
||||
name="choice"
|
||||
class="box"
|
||||
id={"choice" + index}
|
||||
checked={currentChoice == index}
|
||||
disabled={state != "ask"}
|
||||
on:change={() => choose(index)} />
|
||||
<label for={"choice" + index}>{ choice.text }</label>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if state == "ask" }
|
||||
<div class="answer-buttons">
|
||||
{#if currentChoice < 0}
|
||||
<button>Skip</button>
|
||||
{:else}
|
||||
<button on:click={() => clearChoice()}>Clear</button>
|
||||
{/if}
|
||||
<button on:click={() => answer(false)} disabled={currentChoice < 0}>Not sure</button>
|
||||
<button on:click={() => answer(true)} disabled={currentChoice < 0}>I'm sure</button>
|
||||
</div>
|
||||
{:else}
|
||||
{#if wasCorrect }
|
||||
<p>you Got it!</p>
|
||||
{:else}
|
||||
<p>you Failed</p>
|
||||
{/if}
|
||||
<div> <button on:click={() => goBack()}>Go back</button> </div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.quiz-box {
|
||||
border: 1px solid gray;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
|
||||
.choices {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
flex-grow: 1;
|
||||
margin: 12px;
|
||||
width: 100%;
|
||||
|
||||
input.box {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input.box:not(:checked) + label {
|
||||
border: 2px solid #ccc;
|
||||
}
|
||||
|
||||
input.box:checked + label {
|
||||
border: 2px solid red;
|
||||
background-color: lighten(red, 45%);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.answer-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
button {
|
||||
margin: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1 +0,0 @@
|
|||
console.log("META", import.meta.env);
|
|
@ -58,6 +58,7 @@
|
|||
min-height: 100vh;
|
||||
|
||||
.content-wrap {
|
||||
border-top: 10px solid #c00;
|
||||
padding-bottom: $footer-height;
|
||||
}
|
||||
}
|
||||
|
|
23
web/src/routes/api/recommendExercise.ts
Normal file
23
web/src/routes/api/recommendExercise.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
// recommend a NEW activity (not a review) to do for the user based on their
|
||||
// current mastery levels
|
||||
//
|
||||
|
||||
import { Exercise } from "materialdb/db";
|
||||
import type { Sequelize } from "sequelize-typescript";
|
||||
|
||||
export async function get(req) {
|
||||
let db: Sequelize = req.locals.db;
|
||||
console.log("login Status:", req.locals.loginStatus);
|
||||
|
||||
let exercise = await Exercise.findOne({
|
||||
where: {
|
||||
// TODO: join against ExerciseSatisfiesConcept
|
||||
},
|
||||
order: db.random(),
|
||||
});
|
||||
console.log("picked", exercise);
|
||||
|
||||
return {
|
||||
body: { exercise },
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue