STACK-API
This folder contains a standalone REST-API for integration of STACK into external systems. This API was originally designed for the specific needs of the Dynexite examination system.
Deployment
Docker
The STACK API has been designed to be deployed using Docker. Pre-made images are publicly available via a gitlab registry under the identifier registry.git.rwth-aachen.de/medien-public/moodle-stack. The used Dockerfile is available here.
E.g. see https://hub.docker.com/r/mathinstitut/goemaxima for images.
⚠️ NOTE: The pre-built images rely on a recent version of the apache2 webserver, which requires a linux kernel version of 3.17 or newer on the Docker host.
The image requires maxima to be available via http. The URL can be configured via the environment variable MAXIMA_URL and defaults to http://maxima:8080/maxima. An example docker-compose file deploying both stack and maxima in the goemaxima variant is provided below:
version: "3.9"
services:
maxima:
image: mathinstitut/goemaxima:2023121100-latest
tmpfs:
- "/tmp"
restart: unless-stopped
cap_add:
- SETGID
- SETUID
cap_drop:
- ALL
environment:
GOEMAXIMA_QUEUE_LEN: 32
read_only: true
stack:
image: URL to be confirmed
restart: unless-stopped
ports:
- '3080:80'
Manual
The application can also be installed manually, although this variant has only undergone limited testing. Prerequisites are a working installation of PHP 8 and composer:
- Copy the content of this repository to your target server. Only the
./api/publicdirectory should be publicly accessible. - Install the required dependencies by performing
composer installinside the./api/directory. - Copy
./api/config_sample.txt_into a file./api/config.phpand adapt to your needs. - Access the api via the
api/public/index.phpfile.
Usage instructions
The STACK service implemented in this repository provides a stateless REST-API with three distinct routes, which all expect and produce application/json requests/responses:
- POST /render: Render a stack question
- POST /grade: Grade user input for a question
- POST /validate: Validate a user's input
Render route
The POST /render route is used to render a given question. It expects a JSON document in the post body, which must contain the following fields:
-
questionDefinition: The Moodle-XML-Export of a single STACK question. -
seed: Seed to choose a question variant. Must be contained in the list of deployed variants. If
no seed is provided, the first deployed variant is used. -
renderInputs: String. Response will include HTML renders of the inputs if value other than ''. The input divs will have the value added as a prefix to their name attribute. -
readOnly: boolean. Determines whether rendered inputs are read only.
The response is again a JSON document, with the following fields:
- a string field
questionrender, containing the rendered question text - a string field
questionsamplesolutiontext, containing the rendered general feedback of the question - a string map
questionassets, containing the assets used in the question, see Plots/Assets - a map field
questioninputsmapping an input name to its configuration - an int field
questionseedindicating the seed used for this response - an int array
questionvariantscontaining all variant seeds of the question - an array of arrays
iframesof arguments to create iframes to hold JS panels e.g. JSXGraph, GeoGebra
The input configuration consists of the following fields:
-
validationtype: A number indicating the configured validation type of the input. Possible values are 0 (hidden), 1 (with variable list), 2 (without variable list) and 3 (compact) -
samplesolution: A map from strings to strings, containing the model answer of the input, in its input form. Input types which are rendered as only one input field, contain only an empty string as key, which is mapped to the model answer. More complex input types contain multiple entries, corresponding to different sub inputs, e.g. for matrix entries, or checkboxes. -
samplesolutionrender: The rendered model answer, as latex code. -
configuration: A map of configuration options. See below.
Input Configuration Keys
The following keys can be contained inside the input configuration options. The availability depends on the type of the input. Please consult the STACK documentation to check which options are supported by which types, if availability is not explicitly specified below:
-
type: Indicates the type of the input, e.g.algebraic. Present for all inputs. Possible values are:algebraic,boolean,checkbox,dropdown,equiv,matrix,notes,numerical,radio,singlechar,string,textarea,unitsandvarmatrix. -
boxWidth: Specifies the desired box size of the input. -
alignIf the input is supposed to beleftorrightaligned. -
syntaxHint: The configured Syntax hint of the input. -
syntaxHintType: If the Syntax hint should be displayed as a placeholder, or as initial value. Supported for the typesalgebraic, numerical, string, units -
options: Key-Value Object containing the options for choice like input types. Supportet for the typescheckbox, dropdown, radio -
matrixbrackets: The desired matrix bracket style. One ofmatrixroundbrackets,matrixsquarebrackets,matrixbarbrackets,matrixnobrackets. Supported for the typesmatrixandvarmatrix -
width: Width of the input matrix. Supported for thematrixtype. -
heightHeight of the input matrix. Supported for thematrixtype.
Grade route
The POST /grade route is used to score a given input for a question. The route expects a JSON document in the post body, which must contain the following fields:
-
questionDefinition: The Moodle-XML-Export of a single STACK question. -
seed: Seed to choose a question variant. Must be contained in the list of deployed variants. If
no seed is provided, the first deployed variant is used. -
answers: A map from string to string, containing the answers.
For input rendered as single fields, one entry inside the answers map, with the input name as key is expected. More complex input types use multiple entries, with the input name as a prefix, e.g. matrix inputs.
The grading route returns the following fields:
- a boolean field
isgradable, indicating if the question could be graded. Possibly false e.g. because of missing inputs - a float field
scorecontaining the obtained score. (Also contained in scores but kept here for backward compatibility.) - a map from the PRT names to floats
scores, containing the marks for each part.score['total']contains the total score for the question. - a map from the PRT names to floats
scoreweights, containing the weighting for each part.scoreweights['total']contains the default total mark for the question. The mark for a question part is itsscore[prt] * scoreweights[prt] * scoreweights['total']. - a string field
specificfeedbackcontaining the rendered specific feedback text - a map from the PRT names to strings
prts, containing the rendered PRT feedback - a string map
gradingassets, containing a list of assets used in the grading response, see Plots/Assets - a string field
responsesummarycontaining a summary of response. (See Reporting.) - an array of arrays
iframesof arguments to create iframes to hold JS panels e.g. JSXGraph, GeoGebra
Validate route
The POST /validate route is used to get validation feedback for a single input of a question. The route expects a JSON document in the post body containing the following fields:
-
questionDefinition: The Moodle-XML-Export of a single STACK question. -
inputName: The name of the input to be validated. -
answers. A map from string to string, containing the answers.
The validation route returns a string field Validation with the corresponding rendered output and an array of arrays iframes of arguments to create iframes to hold JS panels e.g. JSXGraph, GeoGebra.
Download route
The POST /download route is used to download files created by questions.
-
questionDefinition: The Moodle-XML-Export of a single STACK question. -
seed: Seed to choose a question variant. Must be contained in the list of deployed variants. If
no seed is provided, the first deployed variant is used. -
filename- as specified in the question definition and included in the question render. -
fileid- as specified by the question render.
The requested file is returned.
Rendered CASText format
The API returns rendered CASText as parts of its responses in multiple places. The CASText is output as a single string in an intermediate format, which cannot be directly fed to browsers for display, and requires further processing. Applications using the API have to handle the following cases:
-
Latex: The rendered CASText can contain Latex code, which must be rendered before being displayed to the user, e.g. by MathJax. Latex blocks are always enclosed by either
\[ <latex> \]for display mode latex, or\( <latex> \)for inline mode. -
Substitution Tokens: The rendered CASText can contain substitution tokens, indicating where inputs, input validations or PRT feedback should be inserted. These tokens have the format
[[type:name]], where type can be eitherfeedback,inputorvalidation, and name corresponds to the input or PRT name. It is up to the embedding application to replace these tokens with the appropriate content, depending on the context. - Images: The rendered CASText can contain image tags, which have to be processed as described below: Plots/Assets
Plots/Assets
Any plots generated by stack during rendering or grading, as well as static images embedded inside the question are output as image tags inside the rendered CASText, with a generated filename specified as the src attribute. This ensures that the outputs of the render and grading routes are completely deterministic, which is a desirable property, e.g. to detect duplicate question variants. The API response furthermore includes a mapping from the generated name to a randomized filename, which can be used to retrieve the image, inside the questionassets or gradingassets respectively. The images can be downloaded under the url /plots/<filename>.<type>. It is up to the embedding application to download these images, and replace the generated names with a usable url for viewing the question.
Multi language content
The API currently supports outputting German and English localization, both for internal messages and as part of multi-language questions. To control which language is selected the Accept-Language HTTP header is parsed. If not present, the default language is English. Note, in order to add additional languages, you will need to include the Moodle language pack directly inside the appropriate /lang/?? folder.
Errors
If an error occurs during processing of a request, a response with a single JSON field message and an appropriate http response code is returned. The provided message is intended for user display.
Limitations
- Questions requiring custom javascript are not supported. This includes questions using JSXGraph.
- If a question uses randomization, it has to contain deployed variants.
- Grading is only done if all inputs are present and valid (which implies non-empty in most cases).
Implementation Details
The implementation of the STACK-Service is based on the source code of the STACK-Moodle-Plugin. One of the design goals is to minimize the required maintenance effort for upgrading to new STACK releases in the future. To reach this goal, all new code resides in the /api folder, and modifications to already existing files have been kept to a minimum as far as possible.
The implementation of the API is based on the Slim micro framework, which is used for routing, error handling and similar boilerplate tasks. The framework is initialized inside the ./api/public/index.php file, where middlewares and route controllers are registered. All added classes reside under the api namespace and can be autoloaded.
Inside the api directory the code is further split in multiple directories:
-
controller: Contains the controllers classes for the different routes. -
docker: Contains files related to the containerization of the service. -
dtos: Contains DTO classes defining the response formats of the routes. -
emulation: Contains files related to the emulation of moodle functionality. -
public: Contains directly web accessible files. -
util: Contains different utility classes. -
vendor: Contains composer dependencies.
Dependencies
Code dependencies of the api implementation are managed using composer. At runtime the service itself is stateless and only depends on an instance of the maxima CAS, which is expected to be reachable via http, under an url provided via the MAXIMA_URL environment variable, or http://maxima:8080/maxima by default.
Docker based development setup
To ease the development process, the Dockerfile contained in the repository contains multiple stages for development, profiling and production deployment. To start developing using a docker container, start the docker-compose stack defined in the file docker-compose.dev.yml. E.g.
docker compose -f docker-compose.dev.yml build
docker compose -f docker-compose.dev.yml up
The required development image will automatically be build. After the stack started, you will be able to access the service via http://localhost:3080. Any performed changes in the PHP code will be visible live. Note, that Maxima is provided by a geomaxima docker image, and this image will not reflect local changes. The development build also contains the xdebug extension, which is configured to connect to host.docker.internal as a debugger, which will resolve to the locale machines ip address when using docker desktop. Please note that the performance of the development setup will be significantly worse than in production.
Docker production setup
A production image of the API can be built using docker-compose.stack.prod.yml. This then needs renamed, tagged and pushed to a suitable repository on Docker hub.
docker compose -f docker-compose.stack.prod.yml build
docker tag docker-stack your-repo/imagename:tag
docker push your-repo/imagename:tag
At this point a user should just need a working Docker setup and an up-to-date docker-compose.yml file that points to the goemaxima and stack-api images:
docker compose -f docker-compose.yml up
This will download the goemaxima and stack-api images and run the containers in an enclosing container. Obviously, config will be the same as for whoever built the stack-api Docker image.
Version numbers will need to match the latest STACK release in docker-compose.dev.yml, docker-compose.yml, config_sample.txt and config.php.
High level overview
When a function of the API is invoked, the contained question definition in the moodle xml format is first converted to an instance of qtype_stack_question, by the StackQuestionLoader class. After the question has been parsed, it is initialized with the provided seed via a call to initialise_question_from_seed. If any runtime errors occur an exception is thrown and will be returned to the user.
After that, for the render route, all desired outputs are extracted from the questions via their access functions, and undergo a post-processing process, in which any contained multi-language tags are substituted, and urls to images are replaced according to the desired output format. Any statically included assets (pluginfiles) are extracted and are treated equally to generated plots.
For the validation route, the get_input_state function is called for the requested input, and its output is passed to the render_validation function of the input.
For the grading route, the controller iterates over the PRTs of the questions, and calls the get_prt_result method for each of them, with the answers provided in the request as parameter. If the evaluation returned an error, or not all necessary inputs are contained in the request, according to the has_necessary_prt_inputs function, a response with isgradable set to false is returned. Otherwise, the scores of the PRTs are aggregated, and the generated feedback undergoes the same post-processing process as described for the render route.
Moodle Emulation
To allow the stack-moodle-plugin to work standalone, some classes and functions which are normally part of moodle itself have been emulated. All source-code written for this purpose is contained in the emulation directory. The central entry point to load the emulation layer is the file MoodleEmulation.php, which is loaded via require_once on the index page. The following individual pieces have been emulated:
- The files questionlib.php and weblib.php have been created as stubs.
- Some constants defined inside of moodle have been copied.
- The
html_writerclass, and some other outputting functions. - Some functions related to localization.
- The plugin settings
- The moodle_exception class
Basic frontend
A basic frontend is provided at http://localhost:3080/stack.php. This should allow you to load the STACK sample questions and try them out. This requires API specific versions of cors.php and stackjsvle.js (to access files and create iframes) which are in the public folder.
Modifications of existing STACK code
The implementation of the standalone api required some modifications to existing STACK code, which could cause issues with future upstream patches. All performed modifications are documented in this section.
Input types
To allow the API to return appropriate data describing input configuration, the abstract stack_input class has been extended with the following methods:
-
get_api_solution($tavalue): Returns the model answer of the input in the same format in which it would be input by the user -
get_api_solution_render($tadisplay): Returns a rendered version of the model answer of this input. -
render_api_data($tavalue): Returns an array of configuration options which should be exposed via the API.
The get_api_solution and get_api_solution_render functions have sensible default implementations, which are only overwritten for more complex input types. The render_api_data function on the other hand is abstract, and needs to be implemented by each concrete input type individually.
Escalated visibility
To be accessible directly, the following property/method visibility have been promoted to public:
- The
searchproperty inside thestack_multilangclass. - The
has_necessary_prt_inputsfunction of theqtype_stack_questionclass.
Minor changes in STACK 4.6.0
- Some new language keys have been added.
- Some imports inside the
question.phpandmathsoutputfilterbase.class.phpfiles have been wrapped inside an if statement, to only be performed in non api contexts. - A new
get_ta_render_for_inputfunction has been added to theqtype_stack_questionclass. - A new
pluginfilesproperty has been added to theqtype_stack_questionclass. -
iframe.block.phphandles plot URLs and iframe creation conditional on context (i.e API vs not API). -
textdownload.block.phpsets the document link href conditionally on context. - A new
mathsoutputapi.class.phpfile has been added.