Commit 39de5d20 authored by platypro's avatar platypro

Initial Commit

parents
[submodule "backend/deps/wjelement"]
path = backend/deps/wjelement
url = https://github.com/netmail-open/wjelement.git
[submodule "backend/deps/lua"]
path = backend/deps/lua
url = https://gitlab.com/platypro/dep-lua-clone.git
[submodule "backend/deps/onion"]
path = backend/deps/onion
url = https://github.com/davidmoreno/onion
This diff is collapsed.
Quiz Grinder
============
This is a programmable quiz generator. Using a combination of Lua and JSON, build exercises for export in many formats. Inside of backend is the generator itself with a RESTful HTTP server for handling live connections.
Problem sets are lists of problems which may be used in a quiz. Each set has a list of questions with option overrides for each one.
The center of the generator is a problem bank. Each problem in the bank consists of a Lua script and a JSON file. The JSON file defines the problem and the Lua script generates it. Within the JSON file is an "options" object which is passed into the Lua script to change it's operation. These settings may be overridden by problem sets.
This is still in early development... see TODO.md to see what needs to be done.
Developers:
* Aeden McClain ([email protected])
Quiz Grinder TODO
=================
General
* [ ] Command-line interface for backend
Generation Modes
* [X] Random
* [ ] One-of-each
Quiz export types
* [X] Network quiz
* [ ] raw JSON export
* [ ] PDF export
Widgets
* [X] TextView
* [ ] SVGView
* [X] NumberInput
* [ ] TextInput
* [ ] ChoiceInput
* Custom widget support???
Docs
* [ ] Building and deploying
* [ ] RESTful API
* [ ] Comand-line usage
* [ ] Creating problems
cmake_minimum_required(VERSION 3.0)
project(quizgrinder C)
set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_C_FLAGS -Wall)
find_package(Threads)
include(GNUInstallDirs)
add_subdirectory(deps)
set(APP_DATA_PATH ${CMAKE_INSTALL_FULL_DATAROOTDIR}/${CMAKE_PROJECT_NAME})
set(APP_VAR_PATH ${CMAKE_INSTALL_FULL_LOCALSTATEDIR}/${CMAKE_PROJECT_NAME})
configure_file("src/paths.h.in" "${CMAKE_CURRENT_BINARY_DIR}/paths.h")
include_directories(
${CMAKE_CURRENT_BINARY_DIR}
${DEPS_INCLUDE}
)
set(SRCS
src/quizgrinder.c
src/exercise.c
src/script.c
src/session.c
src/pcg_basic.c
${PLATYPRO_BASE_LIST_SRCS}
)
add_executable(quizgrinder ${SRCS})
install(TARGETS quizgrinder)
install(DIRECTORY
res/exercise
res/script
res/pset
DESTINATION ${APP_DATA_PATH})
install(DIRECTORY DESTINATION ${APP_VAR_PATH})
target_link_libraries(quizgrinder ${DEPS_LIBS} ${CMAKE_THREAD_LIBS_INIT})
add_subdirectory(wjelement)
add_subdirectory(lua)
# Onion build settings
SET(ONION_USE_SSL false)
SET(ONION_USE_PAM false)
SET(ONION_USE_PTHREADS true)
SET(ONION_USE_PNG false)
SET(ONION_USE_JPEG false)
SET(ONION_USE_XML2 false)
SET(ONION_USE_SYSTEMD false)
SET(ONION_USE_SQLITE3 false)
SET(ONION_USE_REDIS false)
SET(ONION_USE_GC false)
SET(ONION_USE_TESTS false)
SET(ONION_EXAMPLES false)
SET(ONION_USE_BINDINGS_CPP false)
SET(ONION_POLLER default)
add_subdirectory(onion)
set(DEPS_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/wjelement/include ${CMAKE_CURRENT_SOURCE_DIR}/onion/src PARENT_SCOPE)
set(DEPS_LIBS wjelement Lua onion PARENT_SCOPE)
Subproject commit aa9c502fad28f343e83c87bf031c7e6569673cd3
Subproject commit f6b9d9e0689871226d671fb641698974e3f38762
Subproject commit 9c305ddc9072e20b3a23465c6b157f4eb3ebc50e
{
"name" : "Arithmetic",
"calculator" : "banned",
"options" : {
"operation" : "add",
"max_1" : 20,
"max_2" : 20,
"format" : "natural"
}
}
{
"name" : "Arithmetic",
"questions": [
{
"id":"arith",
"options" : {
"operation" : "add",
"max_1" : 100,
"max_2" : 100
}
},
{
"id":"arith",
"options" : {
"operation" : "sub",
"max_1" : 100,
"max_2" : 50
}
},
{
"id":"arith",
"options" : {
"operation" : "mul",
"max_1" : 15,
"max_2" : 15
}
},
{
"id":"arith",
"options" : {
"operation" : "div",
"max_1" : 20,
"max_2" : 20
}
}
]
}
local a = math.random(options["max_1"])
local b = math.random(options["max_2"])
local ans
function doWidget(ans, name, av, bv)
createWidget("numberInput", tostring(ans),
{hint=tostring(av) .. name .. tostring(bv) .. "="})
end
function round(num, numDecimalPlaces)
local mult = 10^(numDecimalPlaces or 0)
return math.floor(num * mult + 0.5) / mult
end
if options["format"] == "real" then
a = a + round(math.random(), 2)
b = b + round(math.random(), 2)
end
if options["operation"] == "add" then
doWidget(a + b, "+", a, b)
createHint("Addition is repeated counting!")
elseif options["operation"] == "sub" then
doWidget(a - b, "-", a, b)
createHint("Subtraction is the opposite of addition!")
elseif options["operation"] == "mul" then
doWidget(a * b, "x", a, b);
createHint("Multiplication is repeated addition!")
elseif options["operation"] == "div" then
doWidget(a, "÷", a*b, b);
createHint("Division is the opposite of multiplication!")
end
//General Headers
#ifndef INC_GENERAL_H_
#define INC_GENERAL_H_
//Public and Private Definitions
#define PUBLIC
#define PRIVATE static
#define NUMELEMENTS(array) sizeof(array) / sizeof(*array)
#ifndef NULL
#define NULL 0
#endif
#include "paths.h"
#include <stdint.h>
#include <stdbool.h>
#endif
/* Aeden McClain (c) 2019
* web: https://www.platypro.net
* email: [email protected]
* License info at bottom.
*
* This file is a part of Quiz Grinder.
*/
#include "common.h"
#include "exercise.h"
#include <dirent.h>
#include <wjelement.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <libgen.h>
#include "quizgrinder.h"
#include "paths.h"
#include "script.h"
#include "keys.h"
char* genPath(char* path, char* dir)
{
size_t len = strlen(path) + strlen(dir) + 2;
char* result = malloc(len);
snprintf(result, len, "%s/%s", path, dir);
return result;
}
char* loadId(char* path, char* id)
{
size_t len;
char* result;
DIR *d;
struct dirent *dir;
len = strlen(id);
d = opendir(path);
while((dir = readdir(d)))
{
if(dir->d_type == DT_REG && !memcmp(basename(dir->d_name), id, len))
{
result = dir->d_name;
break;
}
}
if(result)
{
result = genPath(path, result);
}
closedir(d);
return result;
}
bool exercise_init(PG_STATE* state, char* search)
{
//TODO: Test against schemas
DIR *d;
struct dirent *dir;
d = opendir(search);
while ((dir = readdir(d)))
{
if(dir->d_type == DT_REG)
{
PROBLEMSET* pset = calloc(1, sizeof(PROBLEMSET));
if(state->exercise._problemSets)
state->exercise._problemSets->next = pset;
else state->exercise.problemSets = pset;
state->exercise._problemSets = pset;
char* fpath = genPath(search, dir->d_name);
if(!fpath) continue;
FILE* f = fopen(fpath, "r");
free(fpath);
WJElement question = NULL;
WJReader pset_rdr =
WJROpenFILEDocument(f, NULL, 0);
WJElement pset_element =
WJEOpenDocument(pset_rdr, NULL, NULL, NULL);
WJElement questions = WJEArray(pset_element, KEYLIST(KEY_QUESTION), WJE_GET);
char* nameObj = WJEString(pset_element, KEY_NAME, WJE_GET, NULL);
pset->name = malloc(strlen(nameObj) + 1);
strcpy(pset->name, nameObj);
pset->numExercises = questions->count;
pset->exercises = calloc(pset->numExercises, sizeof(EXERCISEREF));
int i = 0;
while((question = WJEGet(questions, "[]", question)))
{
char* exerId = WJEString(question, KEY_ID, WJE_GET, NULL);
EXERCISEREF* exref = pset->exercises + i;
EXERCISE* exer = NULL;
//First see if exercise is already loaded
EXERCISE* exerFind = state->exercise.stash;
while(exerFind)
{
if(!strcmp(exerFind->id, exerId))
{
exer=exerFind;
break;
}
}
if(!exer)
{
//Load the exercise
exer = calloc(1, sizeof(EXERCISE));
if(state->exercise._stash)
state->exercise._stash->next = exer;
else state->exercise.stash = exer;
state->exercise._stash = exer;
char* exerPath = loadId(APP_EXERCISE_PATH, exerId);
if(!exerPath) continue;
FILE* f = fopen(exerPath, "r");
free(exerPath);
WJReader exer_rdr =
WJROpenFILEDocument(f, NULL, 0);
WJElement exer_element =
WJEOpenDocument(exer_rdr, NULL, NULL, NULL);
exer->doc = exer_element;
exer->id = malloc(strlen(exerId) + 1);
strcpy(exer->id, exerId);
char* scriptPath = loadId(APP_SCRIPT_PATH, exerId);
// script_load frees scriptPath
if(!scriptPath || !script_load(state, exerId, scriptPath)) continue;
}
exref->exercise = exer;
exref->options = WJEObject(NULL, NULL, WJE_NEW);
WJEMergeObjects(exref->options, WJEObject(exer->doc, KEYLIST(KEY_OPTION), WJE_GET), false);
WJEMergeObjects(exref->options, WJEObject(question, KEYLIST(KEY_OPTION), WJE_GET), true);
i++;
}
WJECloseDocument(pset_element);
}
}
return true;
}
QUESTION* exercise_genQuiz(PG_STATE* state, char* name, int* numQuestions)
{
QUESTION* result = NULL;
QUESTION* at = NULL;
PROBLEMSET* pset = NULL, *pi = state->exercise.problemSets;
while(pi)
{
if(!strcmp(pi->name, name))
{
pset = pi;
break;
}
pi = pi->next;
}
if(!pset) return NULL;
time_t t;
srand((unsigned) time(&t));
while(*numQuestions)
{
WJElement doc = WJEObject(NULL, NULL, WJE_NEW);
int exnum = rand() % pset->numExercises;
EXERCISEREF* exercise = &pset->exercises[exnum];
script_genQuiz(state, doc, exercise);
if(!WJEGet(doc, KEYLIST(KEY_WIDGET), NULL))
{
printf("Question %s has no widgets!!\n", exercise->exercise->id);
WJECloseDocument(doc);
continue;
}
if(!result)
{
result = calloc(1, sizeof(QUESTION));
at = result;
}
else
{
at->next = calloc(1, sizeof(QUESTION));
at = at->next;
}
at->doc = doc;
at->calculator =
WJEString(exercise->exercise->doc, KEY_CALCULATOR, WJE_GET, NULL);
at->name =
WJEString(exercise->exercise->doc, KEY_NAME, WJE_GET, NULL);
(*numQuestions) --;
}
return result;
}
void exercise_destroyQuiz(QUESTION* quiz)
{
QUESTION* q = quiz;
while(q)
{
QUESTION* q_next = q->next;
WJECloseDocument(q->doc);
free(q);
q = q_next;
}
}
void exercise_cleanup(PG_STATE* state)
{
PROBLEMSET* set = state->exercise.problemSets;
while(set)
{
PROBLEMSET* set_next = set->next;
free(set);
free(set->exercises);
free(set->name);
set = set_next;
}
EXERCISE* exer = state->exercise.stash;
while(exer)
{
EXERCISE* exer_next = exer->next;
free(exer);
WJECloseDocument(exer->doc);
free(exer->id);
exer = exer_next;
}
}
/* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* Aeden McClain (c) 2019
* web: https://www.platypro.net
* email: [email protected]
* License info at bottom.
*
* This file is a part of Quiz Grinder.
*/
#ifndef INCLUDE_EXERCISE_H
#define INCLUDE_EXERCISE_H
#include <wjelement.h>
struct PG_State;
typedef enum CalcType
{
CALCTYPE_NONE,
CALCTYPE_BASIC,
CALCTYPE_SCIENTIFIC,
CALCTYPE_GRAPHING
} CALCTYPE;
#define CALCTYPE_NONE_ "None"
#define CALCTYPE_BASIC_ "Basic"
#define CALCTYPE_SCIENTIFIC_ "Scientific"
#define CALCTYPE_GRAPHING_ "Graphing"
typedef struct Exercise
{
struct Exercise* next;
WJElement doc;
char* id;
} EXERCISE;
typedef struct ExerciseRef
{
EXERCISE* exercise;
WJElement options;
} EXERCISEREF;
typedef struct ProblemSet
{
struct ProblemSet* next;
char* name;
EXERCISEREF* exercises;
uint64_t numExercises;
} PROBLEMSET;
typedef struct Question
{
struct Question* next;
WJElement doc;
char* calculator;
char* name;
} QUESTION;
typedef struct PG_Exercise
{
struct ProblemSet* problemSets;
struct ProblemSet* _problemSets;
struct Exercise* stash;
struct Exercise* _stash;
} PG_EXERCISE;
extern bool exercise_init(struct PG_State* state, char* search);
extern QUESTION* exercise_genQuiz(struct PG_State* state, char* name, int* numQuestions);
extern void exercise_destroyQuiz(QUESTION* quiz);
extern void exercise_cleanup(struct PG_State* state);
#endif /*INCLUDE_EXERCISE_H*/
/* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* Aeden McClain (c) 2019
* web: https://www.platypro.net
* email: [email protected]
* License info at bottom.
*
* This file is a part of Quiz Grinder.
*/
#ifndef INCLUDE_KEYS_H
#define INCLUDE_KEYS_H
// Commonly used keys
#define KEY_ID "id"
#define KEY_KEY "key"
#define KEY_UID "uid"
#define KEY_WIDGET "widget"
#define KEY_SOLUTION "solution"
#define KEY_HINT "hint"
#define KEY_TYPE "type"
// Session keys
#define KEY_ACTION "action"
#define KEY_PSET "pset"
#define KEY_GIVEFIRST "giveFirst"
#define KEY_GIVEQSET "giveQset"
#define KEY_RESULT "result"
#define KEY_ERROR "error"
// Exercise keys
#define KEY_NAME "name"
#define KEY_QUESTION "question"
#define KEY_CALCULATOR "calculator"
#define KEY_OPTION "option"
#define KEYLIST(k) k "s"
#define KEYCOUNT(k) k "Count"
#endif /* INCLUDE_KEYS_H */
/* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#define APP_DATA_PATH "@[email protected]"
#define APP_IPC_PATH "@[email protected]"
#define APP_PROBSET_PATH APP_DATA_PATH "/pset"
#define APP_SCRIPT_PATH APP_DATA_PATH "/script"
#define APP_EXERCISE_PATH APP_DATA_PATH "/exercise"
#define APP_SCHEMA_PATH APP_DATA_PATH "/schema"
/*
* PCG Random Number Generation for C.
*
* Copyright 2014 Melissa O'Neill <[email protected]>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* For additional information about the PCG random number generation scheme,
* including its license and other licensing options, visit
*
* http://www.pcg-random.org
*/
/*
* This code is derived from the full C implementation, which is in turn
* derived from the canonical C++ PCG implementation. The C++ version
* has many additional features and is preferable if you can use C++ in
* your project.
*/
#include "pcg_basic.h"