Compare commits
No commits in common. "master" and "old" have entirely different histories.
|
@ -1,13 +1,8 @@
|
|||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
[*.{md,svelte,ts,json,rst}]
|
||||
indent_size = 2
|
||||
|
||||
|
|
14
.eslintrc.js
14
.eslintrc.js
|
@ -1,14 +0,0 @@
|
|||
module.exports = {
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
},
|
||||
"extends": ["@nuxtjs/eslint-config-typescript", "plugin:nuxt/recommended", "prettier"],
|
||||
"plugins": [],
|
||||
"rules": {
|
||||
"quote-props": ["error", "always"],
|
||||
"no-unused-vars": ["off"],
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
||||
},
|
||||
};
|
|
@ -1,92 +1,7 @@
|
|||
test.db*
|
||||
node_modules
|
||||
*/package-lock.json
|
||||
!/package-lock.json
|
||||
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
# Logs
|
||||
/logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# IDE / Editor
|
||||
.idea
|
||||
|
||||
# Service worker
|
||||
sw.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Vim swap files
|
||||
*.swp
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
###
|
||||
# Place your Prettier ignore content here
|
||||
|
||||
###
|
||||
# .gitignore content is duplicated here due to https://github.com/prettier/prettier/issues/8506
|
||||
package-lock.json
|
||||
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
# Logs
|
||||
/logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# IDE / Editor
|
||||
.idea
|
||||
|
||||
# Service worker
|
||||
sw.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Vim swap files
|
||||
*.swp
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
useTabs: false,
|
||||
tabWidth: 2,
|
||||
singleQuote: false,
|
||||
trailingComma: "all",
|
||||
printWidth: 100,
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
This project comes in two parts:
|
||||
|
||||
- Lesson material, which lives in `material`, licensed with [CC BY-SA 4.0][2].
|
||||
- Source code, which is everything else in this repository, licensed with [GPL 3.0][1].
|
||||
|
||||
The full legal text of the license can be found in a file called LICENSE within
|
||||
the respective directories.
|
||||
|
||||
[1]: https://www.gnu.org/licenses/quick-guide-gplv3.html
|
||||
[2]: https://creativecommons.org/licenses/by-sa/4.0/
|
80
README.md
80
README.md
|
@ -1,68 +1,22 @@
|
|||
# eduproj
|
||||
education project
|
||||
===
|
||||
|
||||
## Build Setup
|
||||
The education project is just my personal ideal education system. Here are its
|
||||
goals:
|
||||
|
||||
```bash
|
||||
# install dependencies
|
||||
$ npm install
|
||||
- **Learning is measured through mastery, which is measured through tests.**
|
||||
Mastery is similar to what Anki uses and is trained with spaced repetition.
|
||||
Notably, doing well on a test once doesn't indicate complete mastery and
|
||||
failing a test doesn't indicate complete unmastery. Skipping a question
|
||||
lowers mastery a bit less than getting it wrong.
|
||||
|
||||
# serve with hot reload at localhost:3000
|
||||
$ npm run dev
|
||||
- **Learning by doing.** Many different types of activity formats that should
|
||||
all contribute to mastery of the given concepts. In addition, mastery of
|
||||
certain concepts should also backpropagate to the concepts it depends on. The
|
||||
planned list of activity types are:
|
||||
* Classic multiple-choice problems
|
||||
* Short-answer problems (for math)
|
||||
* Write a short program (+ linting)
|
||||
* Write a bigger project
|
||||
|
||||
# build for production and launch server
|
||||
$ npm run build
|
||||
$ npm run start
|
||||
|
||||
# generate static project
|
||||
$ npm run generate
|
||||
```
|
||||
|
||||
For detailed explanation on how things work, check out the [documentation](https://nuxtjs.org).
|
||||
|
||||
## Special Directories
|
||||
|
||||
You can create the following extra directories, some of which have special behaviors. Only `pages` is required; you can delete them if you don't want to use their functionality.
|
||||
|
||||
### `assets`
|
||||
|
||||
The assets directory contains your uncompiled assets such as Stylus or Sass files, images, or fonts.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/assets).
|
||||
|
||||
### `components`
|
||||
|
||||
The components directory contains your Vue.js components. Components make up the different parts of your page and can be reused and imported into your pages, layouts and even other components.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/components).
|
||||
|
||||
### `layouts`
|
||||
|
||||
Layouts are a great help when you want to change the look and feel of your Nuxt app, whether you want to include a sidebar or have distinct layouts for mobile and desktop.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/layouts).
|
||||
|
||||
### `pages`
|
||||
|
||||
This directory contains your application views and routes. Nuxt will read all the `*.vue` files inside this directory and setup Vue Router automatically.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/get-started/routing).
|
||||
|
||||
### `plugins`
|
||||
|
||||
The plugins directory contains JavaScript plugins that you want to run before instantiating the root Vue.js Application. This is the place to add Vue plugins and to inject functions or constants. Every time you need to use `Vue.use()`, you should create a file in `plugins/` and add its path to plugins in `nuxt.config.js`.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/plugins).
|
||||
|
||||
### `static`
|
||||
|
||||
This directory contains your static files. Each file inside this directory is mapped to `/`.
|
||||
|
||||
Example: `/static/robots.txt` is mapped as `/robots.txt`.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/static).
|
||||
|
||||
### `store`
|
||||
|
||||
This directory contains your Vuex store files. Creating a file in this directory automatically activates Vuex.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/store).
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
export const prisma = new PrismaClient();
|
|
@ -1,7 +0,0 @@
|
|||
import { Router } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/claim", async (req, res) => {});
|
||||
|
||||
export default router;
|
20
api/index.ts
20
api/index.ts
|
@ -1,20 +0,0 @@
|
|||
import express, { json as jsonBodyParser } from "express";
|
||||
|
||||
import userRouter from "./user";
|
||||
import gradeRouter from "./grade";
|
||||
|
||||
const app = express();
|
||||
app.use(jsonBodyParser({ "strict": true }));
|
||||
|
||||
app.get("/hello", (_, res) => {
|
||||
res.send("hellosu there");
|
||||
});
|
||||
|
||||
// routes
|
||||
app.use("/grade", gradeRouter);
|
||||
app.use("/user", userRouter);
|
||||
|
||||
export default {
|
||||
"path": "/api",
|
||||
"handler": app,
|
||||
};
|
27
api/user.ts
27
api/user.ts
|
@ -1,27 +0,0 @@
|
|||
import { Router } from "express";
|
||||
import { hash as bcryptHash } from "bcrypt";
|
||||
|
||||
import { prisma } from "./db";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/user/register", async (req, res) => {
|
||||
const body = req.body;
|
||||
|
||||
const email = body.email;
|
||||
const password = body.password;
|
||||
const passwordHash = await bcryptHash(password, 10);
|
||||
|
||||
const newUser = await prisma.user.create({
|
||||
"data": {
|
||||
email,
|
||||
passwordHash,
|
||||
},
|
||||
});
|
||||
|
||||
res.send({
|
||||
"id": newUser.id,
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
BIN
assets/buefy.png
BIN
assets/buefy.png
Binary file not shown.
Before Width: | Height: | Size: 11 KiB |
|
@ -1,39 +0,0 @@
|
|||
<template>
|
||||
<div class="column">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title has-text-grey">
|
||||
{{ title }}
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content has-text-centered">
|
||||
<b-icon :icon="icon" size="is-large" type="is-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<div class="card-footer-item">
|
||||
<span>
|
||||
<slot />
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
"name": "BuefyCard",
|
||||
"props": {
|
||||
"title": {
|
||||
"type": String,
|
||||
"required": true,
|
||||
},
|
||||
"icon": {
|
||||
"type": String,
|
||||
"required": true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1 @@
|
|||
/result
|
|
@ -0,0 +1,14 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
|
||||
let
|
||||
ocamlStudentModule = pkgs.callPackage ./ocamlStudentModule.nix {};
|
||||
in
|
||||
pkgs.stdenv.mkDerivation {
|
||||
name = "edujails";
|
||||
src = ./.;
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
cp ${ocamlStudentModule} $out/bin/ocamlStudentModule
|
||||
'';
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
{ writeTextFile, nsjail }:
|
||||
|
||||
let
|
||||
inner = writeTextFile {
|
||||
name = "ocamlStudentModuleInner";
|
||||
executable = true;
|
||||
text = ''
|
||||
#!/bin/sh
|
||||
INTERFACE_FILE=$1
|
||||
STUDENT_FILE=$2
|
||||
DRIVER_FILE=$3
|
||||
|
||||
ocamlc -o student.cmi $INTERFACE_FILE
|
||||
ocamlc -o student.cmo $STUDENT_FILE
|
||||
ocaml student.cmo $DRIVER_FILE
|
||||
'';
|
||||
};
|
||||
in
|
||||
writeTextFile {
|
||||
name = "ocamlStudentModule";
|
||||
executable = true;
|
||||
text = ''
|
||||
JAIL=$(mktemp -d)
|
||||
mkdir -p $JAIL/bin
|
||||
cp ${inner} $JAIL/bin/ocamlStudentModule
|
||||
${nsjail}/bin/nsjail \
|
||||
-Mo \
|
||||
--chroot $JAIL \
|
||||
-R /bin/sh \
|
||||
-R /bin/ls \
|
||||
/bin/ls
|
||||
# /bin/ocamlStudentModule
|
||||
'';
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<b-navbar class="is-dark" wrapper-class="container">
|
||||
<template #brand>
|
||||
<b-navbar-item tag="router-link" :to="{ path: '/' }">
|
||||
<img src="~assets/buefy.png" alt="Buefy" height="28" />
|
||||
</b-navbar-item>
|
||||
</template>
|
||||
<template #start>
|
||||
<b-navbar-item v-for="(item, key) of items" :key="key" :to="item.to">
|
||||
<b-icon :icon="item.icon" /> {{ item.title }}
|
||||
</b-navbar-item>
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
<b-navbar-item tag="div">
|
||||
<div class="buttons">
|
||||
<a class="button is-primary">
|
||||
<strong>Sign up</strong>
|
||||
</a>
|
||||
<a class="button is-light"> Log in </a>
|
||||
</div>
|
||||
</b-navbar-item>
|
||||
</template>
|
||||
</b-navbar>
|
||||
<section class="main-content columns">
|
||||
<aside class="column is-2 section">
|
||||
<p class="menu-label is-hidden-touch">General</p>
|
||||
</aside>
|
||||
|
||||
<div class="container column is-10">
|
||||
<Nuxt />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
"name": "DefaultLayout",
|
||||
data() {
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"title": "Home",
|
||||
"icon": "home",
|
||||
"to": { "name": "index" },
|
||||
},
|
||||
{
|
||||
"title": "Inspire",
|
||||
"icon": "lightbulb",
|
||||
"to": { "name": "inspire" },
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,3 +0,0 @@
|
|||
/**
|
||||
* Mastery algorithm
|
||||
*/
|
|
@ -0,0 +1,4 @@
|
|||
material
|
||||
===
|
||||
|
||||
Exercises are decoupled from the actual material and can appear wherever they want
|
|
@ -0,0 +1,7 @@
|
|||
title: Functional Programming Basics
|
||||
type: listing
|
||||
content: |
|
||||
This listing contains some of the basics of functional programming.
|
||||
|
||||
- [Data Types](page://fp-datatypes)
|
||||
- [Functions](page://fp-function)
|
|
@ -0,0 +1,16 @@
|
|||
title: Data Types
|
||||
type: concept
|
||||
summary: |
|
||||
|
||||
content: |
|
||||
|
||||
exercises:
|
||||
|
||||
- name: whatType
|
||||
style: multipleChoice
|
||||
description: |
|
||||
Which of the following can be described as a _function_?
|
||||
|
||||
props:
|
||||
choices:
|
||||
- foo: bar
|
|
@ -0,0 +1,46 @@
|
|||
title: Functions
|
||||
type: concept
|
||||
summary: |
|
||||
Functions describe a process of turning *inputs* into *outputs*.
|
||||
|
||||
content: |
|
||||
In a purely mathematical setting, functions typically have one input and one
|
||||
output, but in functional programming, we can usually get around this either
|
||||
by using [tuples][1] or by [currying][2].
|
||||
|
||||
[1]: page://fp-tuples
|
||||
[2]: page://fp-currying
|
||||
|
||||
exercises:
|
||||
|
||||
- name: doubleIt
|
||||
style: gradedProgram
|
||||
description: |
|
||||
Write a function called `doubleIt` that takes an integer and doubles it.
|
||||
|
||||
satisfiesConcept:
|
||||
- fp-function
|
||||
|
||||
graders:
|
||||
ocaml:
|
||||
style: studentModule
|
||||
props:
|
||||
interface: |
|
||||
val doubleIt : int -> int
|
||||
driver: |
|
||||
open List
|
||||
let () = List.iter
|
||||
(fun x -> assert ((doubleIt x) = (x * 2)))
|
||||
(List.init 100 (fun x -> x + 1));
|
||||
|
||||
- name: whichIsFunction
|
||||
style: multipleChoice
|
||||
description: |
|
||||
Which of the following can be described as a _function_?
|
||||
|
||||
concepts:
|
||||
- fp-function
|
||||
|
||||
props:
|
||||
choices:
|
||||
- foo: bar
|
|
@ -1,2 +0,0 @@
|
|||
---
|
||||
---
|
|
@ -1,2 +0,0 @@
|
|||
---
|
||||
---
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
---
|
||||
|
||||
# Introduction to Calculus
|
||||
|
||||
Calculus is a study of how things change at an infinitesimally small level.
|
||||
Although only a theoretical fantasy, the study of calculus can help us model
|
||||
real-life processes of change, even though the level of granularity is
|
||||
different.
|
|
@ -0,0 +1,2 @@
|
|||
/test.db
|
||||
/dist
|
|
@ -0,0 +1,5 @@
|
|||
compile-database
|
||||
===
|
||||
|
||||
This is a script that converts the files from the `material` directory into an
|
||||
sqlite database with some indexing to allow fast search during run-time.
|
|
@ -0,0 +1,3 @@
|
|||
import { init, Page, Exercise, Grader } from "./page";
|
||||
|
||||
export { init, Page, Exercise, Grader };
|
|
@ -0,0 +1,73 @@
|
|||
import { PrimaryKey, ForeignKey, HasMany, Sequelize, Column, Table, Model, DataType } from "sequelize-typescript";
|
||||
|
||||
@Table
|
||||
export class Page extends Model {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
public slug: string;
|
||||
|
||||
@Column(DataType.STRING)
|
||||
public title: string;
|
||||
|
||||
@Column(DataType.STRING)
|
||||
public type: string;
|
||||
|
||||
@Column(DataType.STRING)
|
||||
public content: string;
|
||||
}
|
||||
|
||||
@Table
|
||||
export class Exercise extends Model {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
public name: string;
|
||||
|
||||
@Column(DataType.STRING)
|
||||
public description: string;
|
||||
|
||||
@Column(DataType.STRING)
|
||||
public style: string;
|
||||
|
||||
@Column(DataType.JSON)
|
||||
public props: any;
|
||||
|
||||
@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(DataType.STRING)
|
||||
public exercise_name: string;
|
||||
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
public language: string;
|
||||
|
||||
@Column(DataType.STRING)
|
||||
public style: string;
|
||||
|
||||
@Column(DataType.JSON)
|
||||
public props: any;
|
||||
}
|
||||
|
||||
export async function init(path: string): Promise<Sequelize> {
|
||||
let sequelize = new Sequelize(`sqlite:${path}`, {
|
||||
models: [Page, Exercise, Grader],
|
||||
});
|
||||
await sequelize.sync({ force: true });
|
||||
return sequelize;
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import { readdir, readFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import * as yaml from "js-yaml";
|
||||
import { plainToClass } from "class-transformer";
|
||||
import { validate } from "class-validator";
|
||||
|
||||
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
|
||||
let materials_dir = "../material";
|
||||
let db_file = "test.db";
|
||||
|
||||
let db;
|
||||
let SLUG_RE = /([A-Za-z\-\_]+)/;
|
||||
|
||||
async function loadPageIntoDb(name: string): Promise<void> {
|
||||
let slug_match = name.match(SLUG_RE);
|
||||
let slug = slug_match[0];
|
||||
|
||||
// if this slug has already been loaded into the database, don't do anything
|
||||
let hasPage = await Page.count({ where: { slug }}) > 0;
|
||||
if (hasPage) { return; }
|
||||
|
||||
let path = join(materials_dir, name);
|
||||
let rawData = await readFile(path, { encoding: "utf8" });
|
||||
let parsedData = yaml.load(rawData);
|
||||
let page_cfg = plainToClass(PageConfig, parsedData);
|
||||
await validate(page_cfg);
|
||||
|
||||
// save page
|
||||
let page = new Page({
|
||||
slug,
|
||||
title: page_cfg.title,
|
||||
type: page_cfg.type,
|
||||
content: page_cfg.content,
|
||||
});
|
||||
await page.save();
|
||||
|
||||
// save exercises
|
||||
async function loadExerciseIntoDb(ex_cfg: ExerciseConfig): Promise<void> {
|
||||
let exercise = new Exercise({
|
||||
page_slug: slug,
|
||||
name: ex_cfg.name,
|
||||
style: ex_cfg.style,
|
||||
props: ex_cfg.props,
|
||||
description: ex_cfg.description,
|
||||
});
|
||||
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,
|
||||
exercise_name: ex_cfg.name,
|
||||
language,
|
||||
style: grader_cfg.style,
|
||||
props: grader_cfg.props,
|
||||
});
|
||||
await grader.save();
|
||||
}
|
||||
|
||||
let graders = ex_cfg.graders;
|
||||
if (graders != null) {
|
||||
await Promise.all(Object.entries(graders).map(loadGraderIntoDb));
|
||||
}
|
||||
}
|
||||
|
||||
let exercises = page_cfg.exercises;
|
||||
if (exercises != null) {
|
||||
await Promise.all(exercises.map(loadExerciseIntoDb));
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
db = await init(db_file);
|
||||
|
||||
// TODO: streaming version of readdir when the folder gets big
|
||||
let names = await readdir(materials_dir);
|
||||
|
||||
await Promise.all(names
|
||||
.filter(name => name.toLowerCase().endsWith(".yml"))
|
||||
.map(loadPageIntoDb)
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "materialdb",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"start": "ts-node index.ts",
|
||||
"gen": "tsc --declaration"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.3",
|
||||
"@types/node": "^16.7.4",
|
||||
"@types/validator": "^13.6.3",
|
||||
"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",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sequelize": "^6.6.5",
|
||||
"sequelize-typescript": "^2.1.0",
|
||||
"sqlite3": "^4.2.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
export class Page {
|
||||
public title: string;
|
||||
public type: string;
|
||||
|
||||
public summary?: string;
|
||||
public content: string;
|
||||
public exercises?: Exercise[];
|
||||
};
|
||||
|
||||
export class Exercise {
|
||||
public name: string;
|
||||
public style: string;
|
||||
public description: string;
|
||||
public props: any;
|
||||
public graders?: Grader[];
|
||||
};
|
||||
|
||||
export class Grader {
|
||||
public language: string;
|
||||
public style: string;
|
||||
public props: any;
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"declaration": true,
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
concepts:
|
||||
- learning targets
|
||||
- can relate to other concepts in the following ways:
|
||||
- concept A "depends" on concept B; explaining concept A requires some
|
||||
information from concept B
|
||||
- concept A "optdepends" on concept B
|
||||
- concept A "satisfies" concept B; mastery of concept A implies mastery of
|
||||
concept B
|
||||
|
||||
topics:
|
||||
- groups of concepts
|
||||
- can nest infinitely
|
||||
|
||||
"reviews" are randomly constructed sets of activities
|
||||
|
||||
each user has a mastery level for each concept
|
||||
|
||||
references:
|
||||
- super memo algorithm used by anki: https://en.wikipedia.org/wiki/SuperMemo#Description_of_SM-2_algorithm
|
||||
|
||||
---
|
||||
|
||||
ocaml should have a runner studentModule, which just puts the student code into
|
||||
a file called student.ml
|
||||
|
||||
the material file defines a student.mli, as well as a driver.ml, then they all
|
||||
get called using:
|
||||
|
||||
```
|
||||
ocamlc -c student.mli # produces student.cmi
|
||||
ocamlc -c student.ml # produces student.cmo
|
||||
ocaml student.cmo driver.ml
|
||||
```
|
||||
|
||||
probably should have like $OCAMLCFLAGS in there to be able to customize each
|
||||
step as well
|
||||
|
||||
---
|
||||
|
||||
what are some good classes to start out with?
|
||||
|
||||
- functional programming
|
||||
- ctfs?
|
||||
- possibly a logic class for math
|
||||
- proof class
|
||||
- ML???? look into running it
|
||||
- can we do reading????????????
|
|
@ -1,50 +0,0 @@
|
|||
export default {
|
||||
// Global page headers: https://go.nuxtjs.dev/config-head
|
||||
"head": {
|
||||
"title": "eduproj",
|
||||
"htmlAttrs": {
|
||||
"lang": "en",
|
||||
},
|
||||
"meta": [
|
||||
{ "charset": "utf-8" },
|
||||
{ "name": "viewport", "content": "width=device-width, initial-scale=1" },
|
||||
{ "hid": "description", "name": "description", "content": "" },
|
||||
{ "name": "format-detection", "content": "telephone=no" },
|
||||
],
|
||||
"link": [{ "rel": "icon", "type": "image/x-icon", "href": "/favicon.ico" }],
|
||||
},
|
||||
|
||||
// Global CSS: https://go.nuxtjs.dev/config-css
|
||||
"css": [],
|
||||
|
||||
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
|
||||
"plugins": [],
|
||||
|
||||
// Auto import components: https://go.nuxtjs.dev/config-components
|
||||
"components": true,
|
||||
|
||||
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
|
||||
"buildModules": [
|
||||
// https://go.nuxtjs.dev/typescript
|
||||
"@nuxt/typescript-build",
|
||||
],
|
||||
|
||||
// Modules: https://go.nuxtjs.dev/config-modules
|
||||
"modules": [
|
||||
// https://go.nuxtjs.dev/buefy
|
||||
"nuxt-buefy",
|
||||
// https://go.nuxtjs.dev/axios
|
||||
"@nuxtjs/axios",
|
||||
],
|
||||
|
||||
// Axios module configuration: https://go.nuxtjs.dev/config-axios
|
||||
"axios": {
|
||||
// Workaround to avoid enforcing hard-coded localhost:3000: https://github.com/nuxt-community/axios-module/issues/308
|
||||
"baseURL": "/",
|
||||
},
|
||||
|
||||
// Build Configuration: https://go.nuxtjs.dev/config-build
|
||||
"build": {},
|
||||
|
||||
"serverMiddleware": [{ "path": "/api", "handler": "~/api" }],
|
||||
};
|
File diff suppressed because it is too large
Load Diff
43
package.json
43
package.json
|
@ -1,44 +1,11 @@
|
|||
{
|
||||
"name": "eduproj",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nuxt",
|
||||
"build": "nuxt build",
|
||||
"start": "nuxt start",
|
||||
"generate": "nuxt generate",
|
||||
"lint:js": "eslint --ext \".js,.ts,.vue\" --ignore-path .gitignore .",
|
||||
"lint:prettier": "prettier --check .",
|
||||
"lint": "npm run lint:js && npm run lint:prettier",
|
||||
"lintfix": "prettier --write --list-different . && npm run lint:js -- --fix",
|
||||
"migrate": "prisma migrate dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
"@prisma/client": "^3.7.0",
|
||||
"bcrypt": "^5.0.1",
|
||||
"core-js": "^3.19.3",
|
||||
"express": "^4.17.2",
|
||||
"nuxt": "^2.15.8",
|
||||
"nuxt-buefy": "^0.4.13",
|
||||
"vue": "^2.6.14",
|
||||
"vue-class-component": "^7.2.6",
|
||||
"vue-server-renderer": "^2.6.14",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"webpack": "^4.46.0"
|
||||
"compiledb": "cd materialdb && npm start",
|
||||
"webdev": "cd web && npm run dev"
|
||||
},
|
||||
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.16.3",
|
||||
"@nuxt/types": "^2.15.8",
|
||||
"@nuxt/typescript-build": "^2.1.0",
|
||||
"@nuxtjs/eslint-config-typescript": "^8.0.0",
|
||||
"@nuxtjs/eslint-module": "^3.0.2",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"eslint": "^8.4.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-nuxt": "^3.1.0",
|
||||
"eslint-plugin-vue": "^8.2.0",
|
||||
"prettier": "^2.5.1",
|
||||
"prisma": "^3.7.0"
|
||||
"materialdb": "file:materialdb",
|
||||
"web": "file:web"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
<template>
|
||||
<section class="section">
|
||||
<div>
|
||||
<Manim> </Manim>
|
||||
</div>
|
||||
|
||||
<div class="columns is-mobile">
|
||||
<card title="Free" icon="github">
|
||||
Open source on <a href="https://github.com/buefy/buefy"> GitHub </a>
|
||||
</card>
|
||||
|
||||
<card title="Responsive" icon="cellphone-link">
|
||||
<b class="has-text-grey"> Every </b> component is responsive
|
||||
</card>
|
||||
|
||||
<card title="Modern" icon="alert-decagram">
|
||||
Built with <a href="https://vuejs.org/"> Vue.js </a> and
|
||||
<a href="http://bulma.io/"> Bulma </a>
|
||||
</card>
|
||||
|
||||
<card title="Lightweight" icon="arrange-bring-to-front"> No other internal dependency </card>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Card from "~/components/Card";
|
||||
|
||||
export default {
|
||||
"name": "IndexPage",
|
||||
"components": {
|
||||
Card,
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,67 +0,0 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"passwordHash" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ApiKey" (
|
||||
"key" TEXT NOT NULL PRIMARY KEY,
|
||||
"expires" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"ownerId" INTEGER NOT NULL,
|
||||
CONSTRAINT "ApiKey_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Page" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Concept" (
|
||||
"name" TEXT NOT NULL PRIMARY KEY
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Exercise" (
|
||||
"name" TEXT NOT NULL PRIMARY KEY,
|
||||
"isGroup" BOOLEAN NOT NULL,
|
||||
"parentName" TEXT,
|
||||
"graderConfig" TEXT NOT NULL,
|
||||
"graderType" TEXT NOT NULL,
|
||||
CONSTRAINT "Exercise_parentName_fkey" FOREIGN KEY ("parentName") REFERENCES "Exercise" ("name") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ExerciseInstance" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"exerciseName" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"timeSubmitted" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"timeClaimed" DATETIME NOT NULL,
|
||||
"timeoutSeconds" INTEGER NOT NULL,
|
||||
"timeCompleted" DATETIME,
|
||||
"needsGrading" BOOLEAN NOT NULL,
|
||||
"gradingProgress" TEXT NOT NULL,
|
||||
"gradingStatus" TEXT NOT NULL,
|
||||
"gradingVerdict" TEXT NOT NULL,
|
||||
"gradingFeedback" TEXT NOT NULL,
|
||||
CONSTRAINT "ExerciseInstance_exerciseName_fkey" FOREIGN KEY ("exerciseName") REFERENCES "Exercise" ("name") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "ExerciseInstance_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ExerciseConceptSat" (
|
||||
"exerciseName" TEXT NOT NULL,
|
||||
"conceptName" TEXT NOT NULL,
|
||||
"satisfiesFactor" REAL NOT NULL,
|
||||
|
||||
PRIMARY KEY ("exerciseName", "conceptName"),
|
||||
CONSTRAINT "ExerciseConceptSat_exerciseName_fkey" FOREIGN KEY ("exerciseName") REFERENCES "Exercise" ("name") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "ExerciseConceptSat_conceptName_fkey" FOREIGN KEY ("conceptName") REFERENCES "Concept" ("name") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
|
@ -1,3 +0,0 @@
|
|||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "sqlite"
|
|
@ -1,92 +0,0 @@
|
|||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:./test.db"
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
name String?
|
||||
passwordHash String
|
||||
solvedExercises ExerciseInstance[]
|
||||
apiKeys ApiKey[]
|
||||
}
|
||||
|
||||
model ApiKey {
|
||||
key String @id @default(uuid())
|
||||
expires DateTime @default(now())
|
||||
scopes String // JSON array of scopes
|
||||
|
||||
owner User @relation(fields: [ownerId], references: [id])
|
||||
ownerId Int
|
||||
}
|
||||
|
||||
model Page {
|
||||
id Int @id @default(autoincrement())
|
||||
}
|
||||
|
||||
model Concept {
|
||||
name String @id
|
||||
exercises ExerciseConceptSat[]
|
||||
}
|
||||
|
||||
model Exercise {
|
||||
name String @id
|
||||
concepts ExerciseConceptSat[]
|
||||
instances ExerciseInstance[]
|
||||
|
||||
isGroup Boolean
|
||||
children Exercise[] @relation("Lineage")
|
||||
parent Exercise? @relation("Lineage", fields: [parentName], references: [name])
|
||||
parentName String?
|
||||
|
||||
// https://github.com/prisma/prisma/issues/3786
|
||||
graderConfig String?
|
||||
}
|
||||
|
||||
model ExerciseInstance {
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
exercise Exercise @relation(fields: [exerciseName], references: [name])
|
||||
exerciseName String
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
|
||||
job Job? @relation(fields: [jobId], references: [id])
|
||||
jobId Int?
|
||||
}
|
||||
|
||||
model Job {
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
timeSubmitted DateTime @default(now())
|
||||
timeClaimed DateTime
|
||||
timeoutSeconds Int
|
||||
timeCompleted DateTime?
|
||||
|
||||
gradingProgress String // JSON progress information
|
||||
gradingStatus String
|
||||
gradingVerdict String
|
||||
gradingFeedback String // JSON
|
||||
ExerciseInstance ExerciseInstance[]
|
||||
}
|
||||
|
||||
model ExerciseConceptSat {
|
||||
exercise Exercise @relation(fields: [exerciseName], references: [name])
|
||||
exerciseName String
|
||||
|
||||
concept Concept @relation(fields: [conceptName], references: [name])
|
||||
conceptName String
|
||||
|
||||
satisfiesFactor Float
|
||||
|
||||
@@id([exerciseName, conceptName])
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 8.4 KiB |
|
@ -1,10 +0,0 @@
|
|||
# STORE
|
||||
|
||||
**This directory is not required, you can delete it if you don't want to use it.**
|
||||
|
||||
This directory contains your Vuex Store files.
|
||||
Vuex Store option is implemented in the Nuxt.js framework.
|
||||
|
||||
Creating a file in this directory automatically activates the option in the framework.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2018",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"lib": ["ESNext", "ESNext.AsyncIterable", "DOM"],
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"experimentalDecorators": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./*"],
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"types": ["@nuxt/types", "@nuxtjs/axios", "@types/node"]
|
||||
},
|
||||
"exclude": ["node_modules", ".nuxt", "dist"]
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
|
||||
plugins: ['svelte3', '@typescript-eslint'],
|
||||
ignorePatterns: ['*.cjs'],
|
||||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||
settings: {
|
||||
'svelte3/typescript': () => require('typescript')
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2019
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
}
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
/test.db
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "web",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "svelte-kit dev",
|
||||
"build": "svelte-kit build",
|
||||
"preview": "svelte-kit preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
||||
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
|
||||
},
|
||||
"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",
|
||||
"eslint": "^7.22.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-svelte3": "^3.2.0",
|
||||
"node-sass": "^6.0.1",
|
||||
"prettier": "~2.2.1",
|
||||
"prettier-plugin-svelte": "^2.2.0",
|
||||
"sqlite3": "^5.0.2",
|
||||
"svelte": "^3.34.0",
|
||||
"svelte-check": "^2.0.0",
|
||||
"svelte-preprocess": "^4.0.0",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^4.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@types/materialdb": "file:../materialdb",
|
||||
"materialdb": "file:../materialdb",
|
||||
"monaco-editor": "^0.27.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sequelize": "^6.6.5",
|
||||
"sequelize-typescript": "^2.1.0",
|
||||
"ts-pattern": "^3.2.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Heebo:wght@300;400;700&display=swap" integrity="sha384-TIFtbbKQ3b73InoKF2MDgqFhPYGLQ1h2cnNtGUFVvvd/eRw94RvudHY4y+MMmbw4" crossorigin="anonymous">
|
||||
|
||||
%svelte.head%
|
||||
<style>html, body { margin: 0; paddding: 0; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="svelte">%svelte.body%</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,2 @@
|
|||
/// <reference types="@sveltejs/kit" />
|
||||
/// <reference types="materialdb" />
|
|
@ -0,0 +1,9 @@
|
|||
import { checkLogin } from "$lib/auth";
|
||||
import { dbPromise } from "$lib/db";
|
||||
|
||||
export async function handle({ request, resolve }) {
|
||||
request.locals.db = await dbPromise;
|
||||
request.locals.loginStatus = checkLogin(request);
|
||||
console.log("DB", request.locals.db);
|
||||
return resolve(request);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import Exercise from "$lib/exercise/Exercise.svelte";
|
||||
|
||||
let state = "loading";
|
||||
let exercise = null;
|
||||
|
||||
onMount(async () => {
|
||||
let resp = await fetch("/api/recommendExercise");
|
||||
let body = await resp.json();
|
||||
exercise = body.exercise;
|
||||
state = "finished";
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if state == "loading"}
|
||||
loading...
|
||||
{:else}
|
||||
<Exercise exercise={exercise} />
|
||||
{/if}
|
|
@ -0,0 +1,19 @@
|
|||
import { User } from "$lib/db";
|
||||
import { match, select } from "ts-pattern";
|
||||
|
||||
type _LoginStatus =
|
||||
| { type: "loggedIn"; }
|
||||
| { type: "notLoggedIn"; }
|
||||
|
||||
export class LoginStatus {
|
||||
constructor(
|
||||
public isLoggedIn: boolean,
|
||||
public user_id?: number,
|
||||
public username?: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
export function checkLogin(req: Request): LoginStatus {
|
||||
console.log("req", req);
|
||||
return new LoginStatus(false);
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
|
||||
|
||||
export let language = "ocaml";
|
||||
let editorElement;
|
||||
|
||||
onMount(() => {
|
||||
let editor = monaco.editor.create(editorElement, {
|
||||
language,
|
||||
});
|
||||
editor.layout();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={editorElement} class="editor" />
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.editor {
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import { DataType, Unique, Column, Table, Model } from "sequelize-typescript";
|
||||
|
||||
/// An instance of an exercise, created to be solved by a specific user.
|
||||
@Table
|
||||
export class ExerciseInstance extends Model {
|
||||
@Column(DataType.STRING)
|
||||
public user_id: string;
|
||||
|
||||
@Column(DataType.STRING)
|
||||
public exercise_name: string;
|
||||
|
||||
@Column(DataType.JSON)
|
||||
public instance_props: any;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { Sequelize } from "sequelize-typescript";
|
||||
import { User } from "./user";
|
||||
import { ExerciseInstance } from "./exercise";
|
||||
|
||||
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 init(): Promise<Sequelize> {
|
||||
let sequelize = new Sequelize(`sqlite:test.db`, {
|
||||
models: [User, ExerciseInstance],
|
||||
});
|
||||
await sequelize.sync({ force: true });
|
||||
return sequelize;
|
||||
}
|
||||
|
||||
export let dbPromise = init();
|
||||
export let materialDb = loadMaterialDb();
|
||||
|
||||
export { User, ExerciseInstance };
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
import MultipleChoice from "./MultipleChoice/Component.svelte";
|
||||
import GradedProgram from "./GradedProgram/Component.svelte";
|
||||
import { ExerciseStyle, ExerciseInfo } from "$lib/exercise";
|
||||
|
||||
export let exercise: ExerciseInfo;
|
||||
</script>
|
||||
|
||||
<details open>
|
||||
<summary>Exercise Spec</summary>
|
||||
<pre>{JSON.stringify(exercise, null, 2)}</pre>
|
||||
</details>
|
||||
|
||||
{#if !exercise}
|
||||
loading
|
||||
{:else if exercise.style == ExerciseStyle.MultipleChoice}
|
||||
<MultipleChoice props={exercise.props} />
|
||||
{:else if exercise.style == ExerciseStyle.GradedProgram}
|
||||
<GradedProgram props={exercise.props} />
|
||||
{:else}
|
||||
Sorry, no support for <code>{exercise.style}</code> type of questions yet!
|
||||
{/if}
|
|
@ -0,0 +1,9 @@
|
|||
<script lang="ts">
|
||||
import Editor from "$lib/components/Editor.svelte";
|
||||
|
||||
export let props;
|
||||
</script>
|
||||
|
||||
<code>{props}</code>
|
||||
|
||||
<Editor />
|
|
@ -0,0 +1,9 @@
|
|||
import { ExerciseStyle, IExercise } from "..";
|
||||
import Component from "./Component.svelte";
|
||||
|
||||
let spec: IExercise = {
|
||||
style: ExerciseStyle.GradedProgram,
|
||||
component: Component,
|
||||
};
|
||||
|
||||
export default spec;
|
|
@ -0,0 +1,142 @@
|
|||
<script lang="ts">
|
||||
export let props;
|
||||
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 props.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>
|
|
@ -0,0 +1,7 @@
|
|||
export class Props {
|
||||
|
||||
}
|
||||
|
||||
export class MaskedInfo {
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
// mask the full exercise object to only the part that the client needs to see
|
||||
// in order to present it to the user (so the user can't just peek into network
|
||||
// transactions to see what the correct answer is)
|
||||
|
||||
import type { Exercise } from "materialdb/db";
|
||||
import { ExerciseInstance } from "$lib/db";
|
||||
import type { LoginStatus } from "$lib/auth";
|
||||
import { ExerciseStyle } from ".";
|
||||
|
||||
export class ExerciseInfo {
|
||||
public style: string;
|
||||
public props: any;
|
||||
}
|
||||
|
||||
export async function createInstance(loginStatus: LoginStatus, exercise: Exercise): Promise<ExerciseInfo> {
|
||||
let props;
|
||||
switch (exercise.style) {
|
||||
case ExerciseStyle.GradedProgram:
|
||||
break;
|
||||
case ExerciseStyle.MultipleChoice:
|
||||
break;
|
||||
}
|
||||
|
||||
let instance = new ExerciseInstance({
|
||||
user_id: loginStatus.user_id,
|
||||
exercise_name: exercise.name,
|
||||
props,
|
||||
});
|
||||
instance.save();
|
||||
|
||||
let info = new ExerciseInfo();
|
||||
info.style = exercise.style;
|
||||
info.props = props;
|
||||
console.log("info", info);
|
||||
return info;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { createInstance, ExerciseInfo } from "./createInstance";
|
||||
import type { SvelteComponentDev } from "svelte";
|
||||
|
||||
export enum ExerciseStyle {
|
||||
GradedProgram = "gradedProgram",
|
||||
MultipleChoice = "multipleChoice",
|
||||
}
|
||||
|
||||
export interface IExercise {
|
||||
style: ExerciseStyle;
|
||||
component: SvelteComponentDev;
|
||||
}
|
||||
|
||||
export interface IExerciseProps {
|
||||
}
|
||||
|
||||
export interface IExerciseMaskedInfo {
|
||||
}
|
||||
|
||||
export { createInstance, ExerciseInfo };
|
|
@ -0,0 +1,174 @@
|
|||
<div id="app">
|
||||
<div class="content-wrap">
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<div class="brand"><a href="/">Edu</a></div>
|
||||
<nav class="header-nav">
|
||||
<ul class="list list-reset">
|
||||
<li><a href="/about">About</a></li>
|
||||
<li><a href="/learn">Learn</a></li>
|
||||
<li><a href="/browse">Browse</a></li>
|
||||
<li><a href="/contribute">Contribute</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-blocks">
|
||||
<div class="footer-block">
|
||||
<div class="footer-block-title">Eduproj</div>
|
||||
<ul class="list-reset">
|
||||
<li><a href="https://git.mzhang.io/michael/eduproj" target="_blank">Source Code</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-block">
|
||||
<div class="footer-block-title">For Students</div>
|
||||
<ul class="list-reset">
|
||||
<li><a href="/about">Edu</a></li>
|
||||
<li><a href="/about">Edu</a></li>
|
||||
<li><a href="/about">Edu</a></li>
|
||||
<li><a href="/about">Edu</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-block">
|
||||
<div class="footer-block-title">About Us</div>
|
||||
<ul class="list-reset">
|
||||
<li><a href="/about">About Me</a></li>
|
||||
<li><a href="/about">About Me</a></li>
|
||||
<li><a href="/about">About Me</a></li>
|
||||
<li><a href="/about">About Me</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$footer-height: 180px;
|
||||
|
||||
#app {
|
||||
font-family: Heebo, sans-serif;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
|
||||
.content-wrap {
|
||||
border-top: 10px solid #c00;
|
||||
padding-bottom: $footer-height;
|
||||
|
||||
> .container {
|
||||
padding-bottom: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
width: 750px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container {
|
||||
width: 970px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container {
|
||||
width: 1170px;
|
||||
}
|
||||
}
|
||||
|
||||
.list-reset {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 80px;
|
||||
|
||||
.brand {
|
||||
margin-right: 32px;
|
||||
|
||||
a {
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
flex-grow: 1;
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 0;
|
||||
|
||||
li {
|
||||
margin: 0;
|
||||
box-sizing: inherit;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
padding: 0 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: #eee;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: $footer-height;
|
||||
padding-top: 24px;
|
||||
|
||||
.footer-blocks {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-right: -24px;
|
||||
margin-left: -24px;
|
||||
margin-top: -12px;
|
||||
|
||||
.footer-block {
|
||||
flex-grow: 1;
|
||||
flex-basis: 160px;
|
||||
box-sizing: content-box;
|
||||
padding: 12px 24px;
|
||||
|
||||
.footer-block-title {
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: lighten(black, 20%);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,4 @@
|
|||
<h2>How does it work?</h2>
|
||||
|
||||
<ul>
|
||||
</ul>
|
|
@ -0,0 +1,29 @@
|
|||
// 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";
|
||||
import type { LoginStatus } from "$lib/auth";
|
||||
import { createInstance } from "$lib/exercise";
|
||||
|
||||
export async function get(req) {
|
||||
let db: Sequelize = req.locals.db;
|
||||
let loginStatus: LoginStatus = req.locals.loginStatus;
|
||||
console.log("login Status:", loginStatus);
|
||||
|
||||
let exercise = await Exercise.findOne({
|
||||
where: {
|
||||
// TODO: join against ExerciseSatisfiesConcept
|
||||
},
|
||||
order: db.random(),
|
||||
});
|
||||
console.log("picked", exercise);
|
||||
|
||||
let instance = await createInstance(loginStatus, exercise);
|
||||
console.log("instance", instance);
|
||||
|
||||
return {
|
||||
body: { exercise: instance },
|
||||
};
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { User } from "$lib/db";
|
||||
|
||||
export async function post(req) {
|
||||
// get the activity from the database
|
||||
|
||||
return {
|
||||
body: { count: await User.count() },
|
||||
};
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
import MasteryDemo from "$lib/MasteryDemo.svelte";
|
||||
</script>
|
||||
|
||||
<h1>Learn by mastery</h1>
|
||||
|
||||
<MasteryDemo />
|
||||
|
||||
<p>
|
||||
yadda yadda yadda what's unique about this mastery based learning appraoch
|
||||
</p>
|
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,15 @@
|
|||
import preprocess from 'svelte-preprocess';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: preprocess(),
|
||||
|
||||
kit: {
|
||||
// hydrate the <div id="svelte"> element in src/app.html
|
||||
target: '#svelte'
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"module": "es2020",
|
||||
"lib": ["es2020", "DOM"],
|
||||
"target": "es2020",
|
||||
/**
|
||||
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
|
||||
to enforce using \`import type\` instead of \`import\` for Types.
|
||||
*/
|
||||
"importsNotUsedAsValues": "error",
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
To have warnings/errors of the Svelte compiler at the correct position,
|
||||
enable source maps by default.
|
||||
*/
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"baseUrl": ".",
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"paths": {
|
||||
"$lib": ["src/lib"],
|
||||
"$lib/*": ["src/lib/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
|
||||
}
|
Loading…
Reference in New Issue