Commit 8790f7ef authored by platypro's avatar platypro

Code refactoring.

- Web front-end was switched to typescript + webpack
- Backend widget code moved to /content
- Name switched to QuizGrind
- Bug fixes
parent a05da910
[submodule "backend/deps/wjelement"]
path = backend/deps/wjelement
[submodule "deps/wjelement"]
path = deps/wjelement
url = https://github.com/platypro/wjelement.git
[submodule "backend/deps/lua"]
path = backend/deps/lua
[submodule "deps/lua"]
path = deps/lua
url = https://gitlab.com/platypro/dep-lua-clone.git
[submodule "backend/deps/onion"]
path = backend/deps/onion
[submodule "deps/onion"]
path = deps/onion
url = https://github.com/davidmoreno/onion
[submodule "backend/deps/libpmustache"]
path = backend/deps/libpmustache
url = https://gitlab.com/platypro/libpmustache.git
[submodule "deps/libpmustache"]
path = deps/libpmustache
url = https://gitlab.com/platypro/libpmustache.git
cmake_minimum_required(VERSION 3.0)
project(quizgrind C)
option(QUIZGRIND_BUILD_PDF "PDF Support" ON)
set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_C_FLAGS "-Wall -O0")
include(GNUInstallDirs)
set(APP_DATA_PATH ${CMAKE_INSTALL_FULL_DATAROOTDIR}/${CMAKE_PROJECT_NAME})
set(APP_VAR_PATH ${CMAKE_INSTALL_FULL_LOCALSTATEDIR}/${CMAKE_PROJECT_NAME})
add_subdirectory(deps)
add_subdirectory(backend)
Quiz Grinder
============
QuizGrind
=========
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.
......
Quiz Grinder TODO
=================
QuizGrind TODO
==============
General
* [X] Command-line interface for backend
......
cmake_minimum_required(VERSION 3.0)
project(quizgrinder C)
set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_C_FLAGS "-Wall -O0")
option(QUIZGRINDER_BUILD_PDF "PDF Support" ON)
find_package(Threads)
find_package(PkgConfig)
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})
set(CMAKE_INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR} ${CMAKE_INSTALL_PREFIX}/lib)
configure_file("src/paths.h.in" "${CMAKE_CURRENT_BINARY_DIR}/paths.h")
configure_file("paths.h.in" "${CMAKE_CURRENT_BINARY_DIR}/paths.h")
set(SRCS
src/quizgrinder.c
src/exercise.c
src/script.c
src/daemon/daemon.c
src/daemon/http.c
src/pcg_basic.c
quizgrind.c
exercise.c
script.c
widget.c
daemon/daemon.c
daemon/http.c
pcg_basic.c
# Widget sources
../content/widget/choiceInput/backend.c
../content/widget/imageView/backend.c
../content/widget/lineInput/backend.c
../content/widget/numberInput/backend.c
../content/widget/textInput/backend.c
../content/widget/textView/backend.c
${DEPS_SRC}
)
)
IF(QUIZGRINDER_BUILD_PDF)
IF(QUIZGRIND_BUILD_PDF)
pkg_search_module(CAIRO cairo)
pkg_search_module(RSVG librsvg-2.0)
pkg_search_module(PANGO pango)
pkg_search_module(PANGOCAIRO pangocairo)
set(SRCS ${SRCS} src/pdf.c)
add_definitions(-D_QUIZGRINDER_BUILD_PDF)
set(SRCS ${SRCS} pdf.c)
add_definitions(-D_QUIZGRIND_BUILD_PDF)
ENDIF()
add_executable(quizgrinder ${SRCS})
add_executable(quizgrind ${SRCS})
install(TARGETS quizgrinder DESTINATION ${CMAKE_INSTALL_FULL_BINDIR})
install(TARGETS quizgrind DESTINATION ${CMAKE_INSTALL_FULL_BINDIR})
install(DIRECTORY
../content/exercise
../content/pset
DESTINATION ${APP_DATA_PATH})
install(DIRECTORY DESTINATION ${APP_VAR_PATH})
target_link_libraries(quizgrinder
target_link_libraries(quizgrind
${DEPS_LIBS}
${CMAKE_THREAD_LIBS_INIT}
)
target_include_directories(quizgrinder PUBLIC
target_include_directories(quizgrind PUBLIC
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}
../content/widget
${DEPS_INCLUDE}
src
)
if(QUIZGRINDER_BUILD_PDF)
target_link_libraries(quizgrinder
if(QUIZGRIND_BUILD_PDF)
target_link_libraries(quizgrind
${CAIRO_LIBRARIES}
${RSVG_LIBRARIES}
${PANGO_LIBRARIES}
${PANGOCAIRO_LIBRARIES}
)
message(STATUS ${RSVG_INCLUDE_DIRS})
target_include_directories(quizgrinder PUBLIC
target_include_directories(quizgrind PUBLIC
${CAIRO_INCLUDE_DIRS}
${RSVG_INCLUDE_DIRS}
${PANGO_INCLUDE_DIRS}
......
#include "common.h"
#include "daemon/daemon.h"
#include <termios.h>
#include <pcg_basic.h>
#include <unistd.h>
#include "daemon/http.h"
#include "quizgrinder.h"
bool daemon_stop(PG_STATE* state)
{
if(state->daemon.type & DAEMON_HTTP)
daemon_http_stop(state);
tcsetattr(STDIN_FILENO, TCSANOW, &state->daemon.oldt);
return true;
}
bool daemon_run(PG_STATE* state)
{
if(state->daemon.type & DAEMON_HTTP)
daemon_http_start(state);
else return false;
printf("Daemon mode enabled\n");
struct termios term;
char c;
exercise_loadAll(state);
pcg32_srandom(time(NULL), (intptr_t)&printf);
/* Aeden McClain (c) 2019
* web: https://www.platypro.net
* email: dev@platypro.net
* License info at bottom.
*
* This file is a part of QuizGrind.
*/
#include "common.h"
#include "daemon/daemon.h"
#include <termios.h>
#include <pcg_basic.h>
#include <unistd.h>
#include "daemon/http.h"
#include "quizgrind.h"
bool daemon_stop(PG_STATE* state)
{
if(state->daemon.type & DAEMON_HTTP)
daemon_http_stop(state);
tcsetattr(STDIN_FILENO, TCSANOW, &state->daemon.oldt);
return true;
}
bool daemon_run(PG_STATE* state)
{
if(state->daemon.type & DAEMON_HTTP)
daemon_http_start(state);
else return false;
printf("Daemon mode enabled\n");
struct termios term;
char c;
exercise_loadAll(state);
pcg32_srandom(time(NULL), (intptr_t)&printf);
char link[256];
ssize_t rval;
rval = readlink("/proc/self/fd/1", link, sizeof(link));
link[rval] = '\0';
//Change term settings
if (tcgetattr( STDIN_FILENO, &term) == 0)
{
state->daemon.oldt = term;
term.c_lflag &= ~(ICANON | ECHO | ISIG);
tcsetattr( STDIN_FILENO, TCSANOW, &term);
}
while((c = getchar()) != 'q')
{
if(c == -1)
{
daemon_http_join(state);
break;
}
switch(c)
{
case 'r':
// This can probably be optimized greatly. For now this is fine
printf("Reloading...\n");
exercise_cleanup(state);
script_cleanup(state);
script_init(state);
exercise_loadAll(state);
printf("Reloaded!\n");
break;
default: continue;
}
}
daemon_stop(state);
return true;
}
//Change term settings
if (tcgetattr( STDIN_FILENO, &term) == 0)
{
state->daemon.oldt = term;
term.c_lflag &= ~(ICANON | ECHO | ISIG);
tcsetattr( STDIN_FILENO, TCSANOW, &term);
}
while((c = getchar()) != 'q')
{
if(c == -1)
{
daemon_http_join(state);
break;
}
switch(c)
{
case 'r':
// This can probably be optimized greatly. For now this is fine
printf("Reloading...\n");
exercise_cleanup(state);
script_cleanup(state);
script_init(state);
exercise_loadAll(state);
printf("Reloaded!\n");
break;
default: continue;
}
}
daemon_stop(state);
return true;
}
/* 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: dev@platypro.net
* License info at bottom.
*
* This file is a part of QuizGrind.
*/
#ifndef INCLUDE_DAEMON_H
#define INCLUDE_DAEMON_H
#include <termios.h>
struct PG_State;
#define DAEMON_HTTP (1<<0)
typedef struct PG_Daemon {
struct termios oldt;
uint8_t type;
} PG_DAEMON;
extern bool daemon_run(struct PG_State* state);
extern bool daemon_stop(struct PG_State* state);
#endif /* INCLUDE_DAEMON_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/>.
*/
......@@ -3,7 +3,7 @@
* email: dev@platypro.net
* License info at bottom.
*
* This file is a part of Quiz Grinder.
* This file is a part of QuizGrind.
*/
#include "common.h"
......@@ -19,7 +19,7 @@
#include <onion/dict.h>
#include "pcg_basic.h"
#include "quizgrinder.h"
#include "quizgrind.h"
#include "keys.h"
USER** getUser(PG_STATE* state, UID_T uid)
......@@ -208,25 +208,27 @@ bool session_common_process(PG_STATE* state, WJElement request, WJWriter respons
USER* u;
if((u = getUserAndAssert(request, state, response)) && u->questionAt)
{
WJElement usolution, usolution_orig = WJEArray(request, KEY_SOLUTION, WJE_GET);
WJElement usolution = WJEArray(request, KEY_SOLUTION, WJE_GET);
SCP_ASSERT(usolution_orig, "No answer given!");
SCP_ASSERT(usolution, "No answer given!");
WJWString(KEY_ACTION, ACTION_QUESTION_CHECK, true, response);
if(!exercise_checkAnswer(u->questionAt, &usolution_orig))
if(!exercise_writeJSONcheck(u->questionAt, usolution, response))
WJWString(KEY_RESULT, "correct", true, response);
else
{
WJWString(KEY_RESULT, "incorrect", true, response);
WJWOpenArray(KEY_ERROR, response);
usolution = usolution_orig->child;
while(usolution)
{
WJWUInt32(NULL, WJEUInt32(usolution, KEY_ID, WJE_GET, 0), response);
usolution = usolution->next;
}
WJWCloseArray(response);
}
// if(!exercise_checkAnswer(u->questionAt, &usolution))
// else
// {
// WJWOpenArray(KEY_ERROR, response);
// usolution = usolution->child;
// while(usolution)
// {
// WJWUInt32(NULL, WJEUInt32(usolution, KEY_ID, WJE_GET, 0), response);
// usolution = usolution->next;
// }
// WJWCloseArray(response);
// }
}
}
else if(!strcmp(action, ACTION_QSET_QUERY))
......
......@@ -3,7 +3,7 @@
* email: dev@platypro.net
* License info at bottom.
*
* This file is a part of Quiz Grinder.
* This file is a part of QuizGrind.
*/
#ifndef INCLUDE_SESSION_H
......
......@@ -3,7 +3,7 @@
* email: dev@platypro.net
* License info at bottom.
*
* This file is a part of Quiz Grinder.
* This file is a part of QuizGrind.
*/
#include "common.h"
......@@ -21,9 +21,10 @@
#include <errno.h>
#include <pmustache/template.h>
#include <pmustache/provider.wje.h>
#include <widget.h>
#include "pcg_basic.h"
#include "quizgrinder.h"
#include "quizgrind.h"
#include "paths.h"
#include "script.h"
#include "keys.h"
......@@ -96,7 +97,7 @@ EXERTEMPLATE* exercise_mkTemplate(PG_STATE* state, QUESTION* q, char* name)
// Compares WJReader string argument to match list.
// Returns the match found (1-based). Otherwise returns 0 when not matching
// Match list has to be in alphabetical (ASCII) order
uint16_t WJRStrCmp(WJReader r, char** match, uint16_t match_count)
uint16_t WJRStrCmp(WJReader r, char** match, uint16_t match_count, size_t size, size_t offset)
{
uint16_t bound_lower = 0;
uint16_t at = 0;
......@@ -106,7 +107,7 @@ uint16_t WJRStrCmp(WJReader r, char** match, uint16_t match_count)
for(char* cmp = WJRString(&complete, r); *cmp; cmp++)
{
char cmp2;
while((cmp2 = match[bound_lower][at]) < *cmp)
while((cmp2 = ((*((char**)(((char*)match) + bound_lower * size + offset)))[at])) < *cmp)
{
bound_lower++;
if(bound_lower == match_count) return 0;
......@@ -210,7 +211,7 @@ char* exercise_load(PG_STATE* state, char* filename)
else if(_IS(name, WJR_TYPE_STRING, KEY_TYPE))
{
char* lst[] = {STATTYPE_PURE_, STATTYPE_STATIC_};
exer->staticType = WJRStrCmp(exer_rdr, lst, 2);
exer->staticType = WJRStrCmp(exer_rdr, lst, 2, sizeof(char*), 0);
}
else if(_IS(name, WJR_TYPE_ARRAY, KEYLIST(KEY_HINT)))
exer->staticQuestion.hints = WJEOpenDocument(exer_rdr, name, NULL, NULL);
......@@ -259,7 +260,13 @@ char* exercise_load(PG_STATE* state, char* filename)
if(_IS(welem, WJR_TYPE_STRING, KEY_KEY))
w->key = WJRStringLoad(NULL, exer_rdr);
else if(_IS(welem, WJR_TYPE_STRING, KEY_TYPE))
w->type = WJRStringLoad(NULL, exer_rdr);
{
w->type = widget_defs + WJRStrCmp(
exer_rdr, (char**)widget_defs,
num_widget_defs,
sizeof(WIDGETDEFINITION),
offsetof(WIDGETDEFINITION, name)) - 1;
}
else if(_IS(welem, WJR_TYPE_OBJECT, KEYLIST(KEY_OPTION)))
w->options = WJEOpenDocument(exer_rdr, welem, NULL, NULL);
}
......@@ -564,7 +571,7 @@ bool exercise_writeJSONWidgets(struct PG_State* state, QUESTION* question, WJWri
{
WJWOpenObject(NULL, w);
WJWUInt32("id", wid->id, w);
WJWString("type", wid->type, TRUE, w);
WJWString("type", wid->type->name, TRUE, w);
if(wid->options)
writeWidget(state, question, wid->options, w, "options");
......@@ -576,83 +583,90 @@ bool exercise_writeJSONWidgets(struct PG_State* state, QUESTION* question, WJWri
return true;
}
void* exercise_showSolution(void* data, SOLUTIONTYPE type, char* format, va_list args)
{
va_list newargs;
size_t dryrun = 0;
WJWriter writer = data;
va_copy(newargs, args);
dryrun = vsnprintf(NULL, 0, format, newargs);
va_end(newargs);
char* buffer = alloca(dryrun + 1);
va_copy(newargs, args);
vsnprintf(buffer, dryrun + 1, format, newargs);
va_end(newargs);
WJWOpenObject(NULL, writer);
WJWString(KEY_KEY, buffer, TRUE, writer);
WJWString(KEY_TYPE,
type == SOLTYPE_INPUT ? SOLTYPE_INPUT_
: SOLTYPE_NORMAL_,
TRUE, writer);
WJWCloseObject(writer);
return data;
}
bool exercise_writeJSONSolutions(struct PG_State* state, QUESTION* question, WJWriter w)
{
WIDGET* wid = question->widgets;
WJWOpenArray(KEY_SOLUTION, w);
struct AnswerState ans = {
.data = w,
.fun = exercise_showSolution
};
while(wid)
{
if(wid->key)
{
WJWOpenObject(NULL, w);
WJWUInt32(KEY_ID, wid->id, w);
WJWString(KEY_KEY, wid->key, TRUE, w);
WJWCloseObject(w);
}
widget_answer_show(wid,
question,
&ans);
wid = wid->next;
}
WJWCloseArray(w);
return true;
}
bool widget_checkAnswer(WIDGET* w, char* ans, char* uans)
uint32_t exercise_writeJSONcheck(QUESTION* question, WJElement usolution_base, WJWriter out)
{
if(!ans || !uans) return false;
if(!strcmp(w->type, WIDGET_TEXT_INPUT))
{
if(!WJEBool(w->options, KEY_TEXTINPUT_CAPS, WJE_GET, FALSE))
{
while (tolower(*ans) && (tolower(*ans) == tolower(*uans)))
ans++, uans++;
return (tolower(*ans) == tolower(*uans));
}
return !strcmp(ans, uans);
}
else if(!strcmp(w->type, WIDGET_NUMBER_INPUT))
{
char* end;
double nuans = strtod(uans, &end);
if(*end) return false;
double nans = strtod( ans, &end);
if(*end) return false;
uint32_t mindp = WJEUInt32(w->options, KEY_NUMBERINPUT_MINDP, WJE_GET, FALSE);
double exponent = pow(10, mindp);
return (round(nuans * exponent) == round(nans * exponent));
}
else if(!strcmp(w->type, WIDGET_CHOICE_INPUT))
{
// This has a specific format and length, just compare it.
return !strcmp(ans, uans);
}
return false;
}
uint8_t exercise_checkAnswer(QUESTION* question, WJElement* usolution_orig)
{
WJElement usolution, usolution_next;
WJElement usolution;
WIDGET* widget = question->widgets;
uint32_t result = 0;
while(widget)
{
for(
usolution = (*usolution_orig)->child, usolution_next = usolution->next;
usolution;
//This is to allow deletions but still continue the loop
(usolution = usolution_next) && (usolution_next = usolution_next->next)
)
bool correct = false;
if(widget->type->answer_check != widget_no_answer_check)
{
if(widget->id == WJEUInt32(usolution, KEY_ID, WJE_GET, 0)
&& widget_checkAnswer(widget, widget->key,
WJEString(usolution, KEY_KEY, WJE_GET, NULL)))
for(usolution = usolution_base->child; usolution; usolution = usolution->next)
{
WJEDetach(usolution);
WJECloseDocument(usolution);
if(widget->id == WJEUInt32(usolution, KEY_ID, WJE_GET, 0)
&& widget_answer_check(widget,
WJEString(usolution, KEY_KEY, WJE_GET, NULL)))
{
correct = true;
break;
}
}
} else correct = true;
if(!correct)
{
if(!result) WJWOpenArray(KEY_ERROR, out);
result++;
WJWUInt32(NULL, widget->id, out);
}
widget = widget->next;
}
if(result) WJWCloseArray(out);
return (*usolution_orig)->count;
return result;
}
bool destroyQ_internal(QUESTION* quiz, bool freeWidgetFields)
......@@ -664,7 +678,6 @@ bool destroyQ_internal(QUESTION* quiz, bool freeWidgetFields)
{
WIDGET* widget_next = widget->next;
WJECloseDocument(widget->options);
free(widget->type);
if(freeWidgetFields)
{
free(widget->key);
......
......@@ -3,7 +3,7 @@
* email: dev@platypro.net
* License info at bottom.
*
* This file is a part of Quiz Grinder.
* This file is a part of QuizGrind.
*/
#ifndef INCLUDE_EXERCISE_H
......@@ -13,6 +13,7 @@
#include <pmustache/template.h>
struct PG_State;
struct Widget;
typedef enum CalcType
{
......@@ -37,13 +38,6 @@ typedef enum StaticType {
#define STATTYPE_PURE_ "pure"
#define STATTYPE_STATIC_ "static"
#define WIDGET_TEXT_INPUT "textInput"
#define WIDGET_TEXT_VIEW "textView"
#define WIDGET_LINE_INPUT "lineInput"
#define WIDGET_NUMBER_INPUT "numberInput"
#define WIDGET_IMAGE_VIEW "imageView"
#define WIDGET_CHOICE_INPUT "choiceInput"
typedef struct Question
{
struct Exercise* eref;
......@@ -97,17 +91,6 @@ typedef struct ExerTemplate
PMUS_BUILDER builder;
} EXERTEMPLATE;
typedef struct Widget
{
struct Widget* next;
uint32_t id;
char* type;
char* key;
WJElement options;
} WIDGET;
typedef struct TemplateContainer
{
struct TemplateContainer* next;
......@@ -178,8 +161,8 @@ extern char* exercise_loadOne(struct PG_State* state, char* set);
extern PROBLEMSET* exercise_getSet(struct PG_State* state, char* name);
extern GENMODE exercise_toGenMode(char* name);
extern EXERTEMPLATE* exercise_mkTemplate(struct PG_State* state, QUESTION* q, char* name);
extern uint8_t exercise_checkAnswer(QUESTION* question, WJElement* solutions);
PROCESSEDSTRING exercise_processString(QUESTION* q, char* src);
extern uint32_t exercise_writeJSONcheck(QUESTION* question, WJElement solutions, WJWriter out);
extern bool exercise_writeJSONWidgets(struct PG_State* state, QUESTION* question, WJWriter w);
extern bool exercise_writeJSONSolutions(struct PG_State* state, QUESTION* question, WJWriter w);
extern void exercise_cleanup(struct PG_State* state);
......
......@@ -3,7 +3,7 @@
* email: dev@platypro.net
* License info at bottom.
*
* This file is a part of Quiz Grinder.
* This file is a part of QuizGrind.
*/
#ifndef INCLUDE_KEYS_H
......@@ -39,22 +39,6 @@
#define KEY_SVG "svg"
#define KEY_IMAGE "image"
// Widget keys
#define KEY_TEXTVIEW_TEXT "text"
#define KEY_TEXTVIEW_SHOWINSOLUTION "showInSolution"
#define KEY_TEXTINPUT_CAPS "caps"