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
|
material
|
||||||
===
|
===
|
||||||
|
|
||||||
|
Exercises are decoupled from the actual material and can appear wherever they want
|
||||||
|
|
|
@ -18,6 +18,9 @@ exercises:
|
||||||
description: |
|
description: |
|
||||||
Write a function called `doubleIt` that takes an integer and doubles it.
|
Write a function called `doubleIt` that takes an integer and doubles it.
|
||||||
|
|
||||||
|
satisfiesConcept:
|
||||||
|
- fp-function
|
||||||
|
|
||||||
graders:
|
graders:
|
||||||
ocaml:
|
ocaml:
|
||||||
style: studentModule
|
style: studentModule
|
||||||
|
@ -35,6 +38,9 @@ exercises:
|
||||||
description: |
|
description: |
|
||||||
Which of the following can be described as a _function_?
|
Which of the following can be described as a _function_?
|
||||||
|
|
||||||
|
concepts:
|
||||||
|
- fp-function
|
||||||
|
|
||||||
graders:
|
graders:
|
||||||
ocaml:
|
ocaml:
|
||||||
style: multipleChoice
|
style: multipleChoice
|
||||||
|
|
1
materialdb/.gitignore
vendored
1
materialdb/.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
/test.db
|
/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
|
@Table
|
||||||
export class Page extends Model {
|
export class Page extends Model {
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@Column
|
@Column(DataType.STRING)
|
||||||
public slug: string;
|
public slug: string;
|
||||||
|
|
||||||
@Column
|
@Column(DataType.STRING)
|
||||||
public title: string;
|
public title: string;
|
||||||
|
|
||||||
@Column
|
@Column(DataType.STRING)
|
||||||
public type: string;
|
public type: string;
|
||||||
|
|
||||||
@Column
|
@Column(DataType.STRING)
|
||||||
public content: string;
|
public content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Table
|
@Table
|
||||||
export class Exercise extends Model {
|
export class Exercise extends Model {
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@Column
|
@Column(DataType.STRING)
|
||||||
public name: string;
|
public name: string;
|
||||||
|
|
||||||
@HasMany(() => Grader)
|
@HasMany(() => Grader)
|
||||||
public graders: 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
|
@Table
|
||||||
export class Grader extends Model {
|
export class Grader extends Model {
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@ForeignKey(() => Exercise)
|
@ForeignKey(() => Exercise)
|
||||||
@Column
|
@Column(DataType.STRING)
|
||||||
public exercise_name: string;
|
public exercise_name: string;
|
||||||
|
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@Column
|
@Column(DataType.STRING)
|
||||||
public language: string;
|
public language: string;
|
||||||
|
|
||||||
@Column
|
@Column(DataType.STRING)
|
||||||
public style: string;
|
public style: string;
|
||||||
|
|
||||||
@Column(DataType.JSON)
|
@Column(DataType.JSON)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as yaml from "js-yaml";
|
||||||
import { plainToClass } from "class-transformer";
|
import { plainToClass } from "class-transformer";
|
||||||
import { validate } from "class-validator";
|
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";
|
import { Page as PageConfig, Exercise as ExerciseConfig, Grader as GraderConfig } from "./page";
|
||||||
|
|
||||||
// TODO: configure this thru cmdline or something later
|
// TODO: configure this thru cmdline or something later
|
||||||
|
@ -45,6 +45,9 @@ async function loadPageIntoDb(name: string): Promise<void> {
|
||||||
});
|
});
|
||||||
await exercise.save();
|
await exercise.save();
|
||||||
|
|
||||||
|
async function loadConceptsIntoDb(concept: string): Promise<void> {
|
||||||
|
};
|
||||||
|
|
||||||
async function loadGraderIntoDb([language, grader_cfg]: [string, GraderConfig]): Promise<void> {
|
async function loadGraderIntoDb([language, grader_cfg]: [string, GraderConfig]): Promise<void> {
|
||||||
let grader = new Grader({
|
let grader = new Grader({
|
||||||
page_slug: slug,
|
page_slug: slug,
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "materialdb",
|
"name": "materialdb",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "ts-node index.ts"
|
"start": "ts-node index.ts",
|
||||||
|
"gen": "tsc --declaration"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/js-yaml": "^4.0.3",
|
"@types/js-yaml": "^4.0.3",
|
||||||
|
@ -10,6 +12,7 @@
|
||||||
"ts-node": "^10.2.1"
|
"ts-node": "^10.2.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"typescript": "^4.4.2",
|
||||||
"class-transformer": "^0.4.0",
|
"class-transformer": "^0.4.0",
|
||||||
"class-validator": "^0.13.1",
|
"class-validator": "^0.13.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true
|
"experimentalDecorators": true,
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "dist"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/kit": "next",
|
"@sveltejs/kit": "next",
|
||||||
"@types/node": "^16.7.5",
|
"@types/node": "^16.7.5",
|
||||||
|
"@types/sequelize": "^4.28.10",
|
||||||
"@types/validator": "^13.6.3",
|
"@types/validator": "^13.6.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.19.0",
|
"@typescript-eslint/eslint-plugin": "^4.19.0",
|
||||||
"@typescript-eslint/parser": "^4.19.0",
|
"@typescript-eslint/parser": "^4.19.0",
|
||||||
|
@ -31,6 +32,8 @@
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/materialdb": "file:../materialdb",
|
||||||
|
"materialdb": "file:../materialdb",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sequelize": "^6.6.5",
|
"sequelize": "^6.6.5",
|
||||||
"sequelize-typescript": "^2.1.0"
|
"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="@sveltejs/kit" />
|
||||||
|
/// <reference types="materialdb" />
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { checkLogin } from "$lib/auth";
|
import { checkLogin } from "$lib/auth";
|
||||||
import { db } from "$lib/db";
|
import { dbPromise } from "$lib/db";
|
||||||
|
|
||||||
export async function handle({ request, resolve }) {
|
export async function handle({ request, resolve }) {
|
||||||
|
request.locals.db = await dbPromise;
|
||||||
request.locals.loginStatus = checkLogin(request);
|
request.locals.loginStatus = checkLogin(request);
|
||||||
request.locals.db = await db;
|
|
||||||
return resolve(request);
|
return resolve(request);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import MultipleChoice from "$lib/activity/MultipleChoice.svelte";
|
import MultipleChoice from "$lib/exercise/MultipleChoice.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MultipleChoice />
|
<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
|
import { Page, Exercise, Grader } from "materialdb/db";
|
||||||
export class User extends Model {
|
|
||||||
@Unique
|
async function loadMaterialDb() {
|
||||||
@Column(DataType.STRING)
|
// TODO: ensure this database is read-only?
|
||||||
public email: string;
|
// 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(): Promise<Sequelize> {
|
||||||
}
|
let sequelize = new Sequelize(`sqlite:test.db`, {
|
||||||
|
|
||||||
async function init(path: string): Promise<Sequelize> {
|
|
||||||
console.log("META", import.meta.env);
|
|
||||||
|
|
||||||
let sequelize = new Sequelize(`sqlite:${path}`, {
|
|
||||||
models: [User],
|
models: [User],
|
||||||
});
|
});
|
||||||
await sequelize.sync({ force: true });
|
await sequelize.sync({ force: true });
|
||||||
return sequelize;
|
return sequelize;
|
||||||
}
|
}
|
||||||
|
|
||||||
export let db = init("test.db");
|
export let dbPromise = init();
|
||||||
export let materialDb = loadMaterialDb();
|
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;
|
min-height: 100vh;
|
||||||
|
|
||||||
.content-wrap {
|
.content-wrap {
|
||||||
|
border-top: 10px solid #c00;
|
||||||
padding-bottom: $footer-height;
|
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