INGInious’ documentation

Installation, configuration and upgrade

Installation and deployment

Supported platforms

INGInious is intended to run on Linux (kernel 3.10+), but can also be run on Windows and macOS thanks to the Docker toolbox.

Dependencies setup

INGInious needs:

RHEL/Cent OS 7.0+, Fedora 24+

The previously mentioned dependencies can be installed, for Cent OS 7.0+ :

# curl -fsSL https://get.docker.com/ | sh #This will setup the Docker repo
# yum install -y epel-release https://centos7.iuscommunity.org/ius-release.rpm
# yum install -y git mongodb mongodb-server gcc libtidy python35u python35u-pip python35u-devel zeromq-devel

Or, for Fedora 24+:

# curl -fsSL https://get.docker.com/ | sh #This will setup the Docker repo
# dnf install -y git mongodb mongodb-server gcc libtidy python3 python3-pip python3-devel zeromq-devel

You may also add openldap-devel if you want to use the LDAP auth plugin and xmlsec1-openssl-devel libtool-ltdl-devel for the SAML2 auth plugin.

Danger

Due to compatibility issues, it is recommended to disable SELinux on the target machine.

You can now start and enable the mongod and docker services:

# systemctl start mongod
# systemctl enable mongod
# systemctl start docker
# systemctl enable docker
Ubuntu 16.04+

The previously mentioned dependencies can be installed, for Ubuntu 16.04+:

# curl -fsSL https://get.docker.com/ | sh #This will setup the Docker repo
# apt-get install git mongodb gcc tidy python3 python3-pip python3-dev libzmq-dev

You may also add libldap2-dev libsasl2-dev libssl-dev if you want to use the LDAP auth plugin and libxmlsec1-dev libltdl-dev for the SAML2 auth plugin

You can now start and enable the mongod and docker services:

# systemctl start mongodb
# systemctl enable mongodb
# systemctl start docker
# systemctl enable docker
OS X 10.9+

We use brew to install some packages. Packages are certainly available too via macPorts.

$ brew install mongodb
$ brew install python3

Follow the instruction of brew to enable mongodb.

The next step is to install Docker for Mac.

Windows

Danger

INGInious rely on Docker to run containers. While Docker is supported on Windows 10 (version 1607), INGInious does not provide support for Windows containers yet.

The recommended way to run INGInious under Windows is by using a Linux virtual machine, for much more simplicity. One can also only run the Docker agent under a Linux virtual machine and run the backend and selected frontend under Windows.

In the later case, you’ll need to install Python 3.5+, MongoDB, LibTidy and LibZMQ.

Installing INGInious

The recommended setup is to install INGInious via pip and the master branch of the INGInious git repository. This allows you to use the latest development version. This version is currently the supported one for issues.

$ pip3 install --upgrade git+https://github.com/UCL-INGI/INGInious.git@v0.5

This will automatically upgrade an existing version.

Note

You may want to enable the LDAP/SAML2 plugin or use FCGI/UWSGI instead of the web.py default webserver. In this case, you have to install more packages: simply add [cgi], [uwgsi], [ldap] or [saml2] to the above command, depending on your needs:

$ pip3 install --upgrade git+https://github.com/UCL-INGI/INGInious.git@v0.5#egg=INGInious[cgi,ldap]

Configuring INGInious

INGInious comes with a mini-LMS web app that provides statistics, groups management, and the INGInious studio, that allows to modify and test your tasks directly in your browser. It supports the LTI interface that allows to interface with Learning Management System via the LTI specification. Any LMS supporting LTI is compatible. This includes Moodle, edX, among many others.

To configure the web app automatically, use the inginious-install CLI.

$ inginious-install

This will help you create the configuration file in the current directory. For manual configuration and details, see Configuration reference.

The detailed inginious-install reference can be found at inginious-install.

Running INGInious

During the configuration step, you were asked to setup either a local or remote backend. In the former case, the frontend will automatically start a local backend and grading agents.

With local backend/agent

To run the frontend, please use the inginious-webapp CLI. This will open a small Python web server and display the url on which it is bind in the console. Some parameters (configuration file, host, port) can be specified. Details are available at inginious-webapp.

With remote backend/agent

To run INInious with a remote backend (and agents), do as follows:

  1. On the backend host, launch the backend (see inginious-backend) :

    inginious-backend tcp://backend-host:2001 tcp://backend-host:2000
    

    The agents will connect on tcp://backend-host:2001 and clients on tcp://backend-host:2000

  2. Possibly on different hosts, launch the Docker and MCQ agents (see inginious-agent-docker and inginious-agent-mcq) :

    inginious-agent-docker tcp://backend-host:2001
    inginious-agent-mcq tcp://backend-host:2001
    
  3. In your INGInious frontend configuration file (see Configuration reference), set backend to :

    backend: tcp://backend-host:2000
    
  4. Run the frontend using inginious-webapp.

Webterm setup

An optional web terminal can be used with INGInious to load the remote SSH debug session. This rely on an external tool.

To install this tool :

$ git clone https://github.com/UCL-INGI/INGInious-xterm
$ cd INGInious-xterm && npm install

You can then launch the tool by running:

$ npm start bind_hostname bind_port debug_host:debug_ports

This will launch the app on http://bind_hostname:bind_port. The debug_host and debug_ports parameters are the debug paramaters on the local (see Configuration reference) or remote (see inginious-agent-docker) Docker agent.

To make the INGInious frontend aware of that application, update your configuration file by setting the webterm field to http://bind_hostname:bind_port (see Configuration reference).

For more information on this tool, please see INGInious-xterm. Please note that INGInious-xterm must be launched using SSL if the frontend is launched using SSL.

Webserver configuration

Warning

In configurations below, environment variables accessible to the application must be explicitly repeated. If you use a local backend with remote Docker daemon, you may need to set the DOCKER_HOST variable. To know the value to set, start a terminal that has access to the docker daemon (the terminal should be able to run docker info), and write echo $DOCKER_HOST. If it returns nothing, just ignore this comment. It is possible that you may need to do the same for the env variable DOCKER_CERT_PATH and DOCKER_TLS_VERIFY too.

Using lighttpd

In production environments, you can use lighttpd in replacement of the built-in Python server. This guide is made for CentOS 7.x.

Install lighttpd with fastcgi:

# yum install lighttpd lighttpd-fastcgi

Add the lighttpd user in the necessary groups, to allow it to launch new containers and to connect to mongodb:

# usermod -aG docker lighttpd
# usermod -aG mongodb lighttpd

Create a folder for INGInious, for example /var/www/INGInious, and change the directory owner to lighttpd:

# mkdir -p /var/www/INGInious
# chown -R lighttpd:lighttpd /var/www/INGInious

Put your configuration file in that folder, as well as your tasks, backup, download, and temporary (if local backend) directories (see Configuring INGInious for more details on these folders).

Once this is done, we can configure lighttpd. First, the file /etc/lighttpd/modules.conf, to load these modules:

server.modules = (
    "mod_access",
    "mod_alias"
)

include "conf.d/compress.conf"
include "conf.d/fastcgi.conf"

You can then replace the content of fastcgi.conf with:

server.modules   += ( "mod_fastcgi" )
server.modules   += ( "mod_rewrite" )

alias.url = (
    "/static/" => "/usr/lib/python3.5/site-packages/inginious/frontend/static/"
)

fastcgi.server = ( "/inginious-webapp" =>
    (( "socket" => "/tmp/fastcgi.socket",
        "bin-path" => "/usr/bin/inginious-webapp",
        "max-procs" => 1,
        "bin-environment" => (
            "INGINIOUS_WEBAPP_HOST" => "0.0.0.0",
            "INGINIOUS_WEBAPP_PORT" => "80",
            "INGINIOUS_WEBAPP_CONFIG" => "/var/www/INGInious/configuration.yaml",
            "REAL_SCRIPT_NAME" => ""
        ),
        "check-local" => "disable"
    ))
)

url.rewrite-once = (
    "^/favicon.ico$" => "/static/icons/favicon.ico",
    "^/static/(.*)$" => "/static/$1",
    "^/(.*)$" => "/inginious-webapp/$1"
)

The INGINIOUS_WEBAPP prefixed environment variables are used to replace the default command line parameters. See inginious-webapp for more details.

The REAL_SCRIPT_NAME environment variable must be specified under lighttpd if you plan to access the application from another path than the specified one. In this case, lighttpd forces to set a non-root path /inginious-webapp, while a root access if wanted, in order to serve static files correctly. Therefore, this environment variable is set to an empty string in addition to the rewrite rule.

Finally, start the server:

# systemctl enable lighttpd
# systemctl start lighttpd
Using Apache

You may also want to use Apache. You should install mod_wsgi. WSGI interfaces are supported through the inginious-webapp script. This guide is made for CentOS 7.x.

Install the following packages (please note that the Python3.5+ version of mod_wsgi is required):

# yum install httpd httpd-devel
# pip3.5 install mod_wsgi

Add the apache user in the necessary groups, to allow it to launch new containers and to connect to mongodb:

# usermod -aG docker apache
# usermod -aG mongodb apache

Create a folder for INGInious, for example /var/www/INGInious, and change the directory owner to apache:

# mkdir -p /var/www/INGInious
# chown -R apache:apache /var/www/INGInious

Put your configuration file in that folder, as well as your tasks, backup, download, and temporary (if local backend) directories (see Configuring INGInious for more details on these folders).

Set the environment variables used by the INGInious CLI scripts in the Apache service environment file (see lighttpd for more details):

# cat  << EOF >> /etc/sysconfig/httpd
INGINIOUS_WEBAPP_CONFIG="/var/www/INGInious/configuration.yaml"
INGINIOUS_WEBAPP_HOST="0.0.0.0"
INGINIOUS_WEBAPP_PORT="80"
EOF
# rm /etc/httpd/conf.d/welcome.conf

Please note that the service environment file /etc/sysconfig/httpd may differ from your distribution and wether it uses systemd or init.

You can then modify your /etc/httpd/conf/httpd.conf file to apply the following rules:

LoadModule wsgi_module /usr/lib64/python3.5/site-packages/mod_wsgi/server/mod_wsgi-py35.cpython-35m-x86_64-linux-gnu.so

WSGIScriptAlias / "/usr/bin/inginious-webapp"
WSGIScriptReloading On

Alias /static /usr/lib/python3.5/site-packages/inginious/frontend/static

<Directory "/usr/bin">
    <Files "inginious-webapp">
        Require all granted
    </Files>
</Directory>

<DirectoryMatch "/usr/lib/python3.5/site-packages/inginious/frontend/static">
    Require all granted
</DirectoryMatch>

Please note that the compiled wsgi module path may differ according to the exact Python version you are running.

Updating INGInious

Updating the sources

To fetch the latest updates on the Git repository master branch :

$ pip3 install --upgrade git+https://github.com/UCL-INGI/INGInious.git

Updating your containers

The provided containers can be automatically updated using:

$ inginious-container-update

For your own or third-party containers, please refer to Creating a new container image.

Updating the configuration

Most of the time, you won’t need to update your configuration. If something goes wrong, backup your existing configuration file(s) and run inginious-install again. For further details, please refer to inginious-install or Configuration reference.

Updating the database

The database scheme may have changed since the last INGInious release. A tool is available to do this migration automatically from your configuration file. Please refer to inginious-database-update.

Configuration reference

Hint

The best way to configure INGInious is to use inginious-install. See Configuring INGInious.

Configuring INGInious is done via a file named configuration.yaml or configuration.lti.yaml. To get started, files named configuration.example.yaml and configuration.lti.example.yaml are provided.

The different entries are :

allow_deletion
false if users cannot delete their accounts (and all related data from database), ``true``otherwise.
allow_registration
false if database registration should be disabled. In this mode no password can be set and accounts are only created via the external authentication systems. true otherwise.
backend

The link to the backend used. You can either set it to local or indicate the address of your manually-managed backend.

  • local. In this mode, which is the default, you have to ensure the docker daemon is local to your machine, or, at least, share the same directory structure. This is typically the case if you use Linux and have a local Docker daemon, or if you use Docker for Mac/Windows, or even docker-machine with local machines. This is the configuration described in this tutorial. You will need a running docker daemon on your machine for this to work. If you can use any Docker client command, like docker info, INGInious should run flawlessly.

    In this mode, a supplementary config option is available, local-config.

  • tcp://xxx.yyy.zzz.aaa:bbbb, udp://xxx.yyy.zzz.aaa:bbbb or ipc:///path/to/your/sock, where the adresses are the ip/socket path of the backend you started manually. This is for advanced users only. See commands inginious-backend and inginious-agent for more information.

backup_directory
Path to the directory where are courses backup are stored in cases of data wiping.
local-config

These configuration options are available only if you set backend:local.

concurrency
Number of concurrent task that can be run by INGInious. By default, it is the number of CPU in your host.
debug_host
Host to which the users should connect in order to access to the debug ssh for containers. Most of the time, just do not indicate this option: the address will be automatically guessed.
debug_ports
Range of port, in the form 64100-64200, to which INGInious can bind SSH debug containers, to allow remote debugging. By default, it is 64100-64200.
tmp_dir
A directory whose absolute path must be available by the docker daemon and INGInious at the same time. By default, it is ./agent_tmp.
log_level
Can be set to INFO, WARN, or DEBUG. Specifies the logging verbosity.
maintenance
Set to true if the webapp must be disabled.
mongo_opt

MongoDB client configuration.

host
MongoDB server address. If your database is user/password-protected, use the following syntax: mongodb://USER:PASSWORD@HOSTNAME/DB_NAME
database
You can change the database name if you want multiple instances or in the case of conflict.
plugins
A list of plugin modules together with configuration options. See Plugins for detailed information on available plugins, including their configuration. Please note that the usage of at least one authentication plugin is mandatory for the webapp.
smtp

Mails can be sent by plugins.

sendername
Email sender name, e.g. : INGInious <no-reply@inginious.org>
host
SMTP server.
port
SMTP port.
username
SMTP username.
password
SMTP password.
starttls
Set to true if TLS is needed.
static_directory
Path to the directory where YAML-defined static pages are located.
superadmins`
A list of super-administrators who have admin access on the whole stored content.
tasks_directory
The path to the directory that contains all the task definitions, grouped by courses. (see Creating a new task)
use_minified_js
Set to true to use the minified version of Javascript scripts, false otherwise.
webterm
Link to the INGInious xterm app with the following syntax: http[s]://host:port. If set, it allows to use in-browser task debug via ssh. (See _webterm_setup for more information)

Plugins

Several plugins are available to complete the INGInious feature set.

External authentication plugins

You can allow account creation from an external authentication source. This will link the external credentials to the INGInious account so that the user can log in INGInious using these credentials in the future. Several authentication plugins are available.

LDAP

Uses an LDAP server to authenticate users.

To enable this plugin, add to your configuration file:

plugins:
    - plugin_module: inginious.frontend.plugins.auth.ldap_auth
      id: <some_id_for_ldap>
      host: "your.ldap.server.com"
      encryption: "ssl" #can be tls or none
      base_dn: "ou=People,dc=info,dc=ucl,dc=ac,dc=be"
      request: "(uid={})",
      name: "LDAP Login"

Most of the parameters are self-explaining, but:

id
is the authentication method id. It must be alphanumerical and different from other external authentication methods.
request
is the request made to the LDAP server to search the user to authentify. “{}” is replaced by the username indicated by the user.
SAML2/Shibboleth

Uses a SAML2-compliant identity provider (such as Shibboleth IdP) to authenticate users.

To enable this plugin, add to your configuration file:

plugins:
    - plugin_module: inginious.frontend.plugins.auth.saml2_auth
        id: <some_id_for_saml2>
        strict: true
        sp:
            entityId: "<your_entity_id>"
            x509cert: "<your_cert>"
            privateKey: "<your_private_key>"
        idp:
            entityId: "https://idp.testshib.org/idp/shibboleth"
            singleSignOnService:
                url: "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO"
                binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
            x509cert: "<idp_cert>"
            additionalX509certs:
                - "<idp_cert>"
        security:
             metadataValidUntil: ""
             metadataCacheDuration: ""
        attributes:
             cn: "urn:oid:2.5.4.3"
             email: "urn:oid:1.3.6.1.4.1.5923.1.1.1.6"
             uid: "urn:oid:0.9.2342.19200300.100.1.1"

id is the authentication method id. It must be alphanumerical and different from other external authentication methods. Your IdP is required to provide at least attributes corresponding to the username, the complete name and the email address. Use the attributes entry for the mapping. The additionalX509certs is a plugin-specific entry to specify several certificates in case your IdP is able to use more than one.

This plugin mainly relies on python3-saml package and configuration parameters are interoperable. Please refer to the package documentation for more detailed configuration parameters. The SP Attribute Consuming Service (ACS) is automatically configured by the plugin.

Facebook/LinkedIn/GitHub/Google

Uses a Facebook/LinkedIn/GitHub/Google application to allow authentication (and possibly sharing) via the network. You need to create an app on the appropriate developer platform in order to use this plugin.

To enable this plugin, add to your configuration file:

plugins:
    - plugin_module: inginious.frontend.plugins.auth.facebook_auth
        id: <some_id_for_facebook>
        debug: false
        client_id: <your_app_id>
        client_secret: <your_app_secret>

id is the authentication method id. client_id and client_secret are the OAuth identifier and secret of the created app. Replace facebook_auth by linkedin_auth, github_auth or google_auth according to your case.

Set debug to true to allow OAuth to be run in debug mode (for instance, if SSL is not yet set up).

Twitter

Uses a Twitter application to allow authentication and sharing via the network. You need to create two apps on the appropriate developer platform in order to use this plugin. One will only have authentication capabilities and the other one will be able to write posts for the user in order to share results.

To enable this plugin, add to your configuration file:

plugins:
    - plugin_module: inginious.frontend.plugins.auth.twitter_auth
      id: twitter
      debug: false
      client_id: <app_id_auth_only>
      client_secret: <app_secret_auth_only>
      share_client_id: <app_id_with_share_rights>
      share_client_secret: <app_secret_with_share_rights>
      user: <user_who_created_the_app>

id is the authentication method id. client_id and client_secret are the OAuth identifier and secret of the created app. Set debug to true to allow OAuth to be run in debug mode (for instance, if SSL is not yet set up).

Scoreboard plugin

This plugin allows to generate course/tasks scoreboards. To enable the plugin, add to your configuration file:

plugins:
    - plugin_module: inginious.frontend.plugins.scoreboard

To define a new scoreboard, an additional field scoreboard must be defined in the course.yaml file associated to a course (See Creating a new course). For instance:

scoreboard:
    - content: ["taskid1"]
      name: "Scoreboard task 1"
    - content: ["taskid2", "taskid3"] # sum of both score is taken as overall score
      name: "Scoreboard for task 2 and 3"
    - content: {"taskid4": 2, "taskid5": 3} # overall score is 2*score of taskid4 + 3*score of taskid5
      name: "Another scoreboard"
      reverse: True

This defines three scoreboards for the course. The first one will create a scoreboard for task id taskid1 and will be displayed as Scoreboard task 1. The second one will create a scoreboard for taskid2 and taskid3 where both scores are added. The last one is more complex and will create a reversed scoreboard for task taskid4 and taskid5 where both scores are wieghted by factor 2 and 3, respectively.

The score used by this plugin for each task must be generated via a key/value custom feedback (see feedback-custom) using the score key. Only the succeeded tasks are taken into account.

Contests plugin

This plugin allows to manage an ACM/ICPC like contest inside a course between students. To enable the plugin, add to your configuration file:

plugins:
    - plugin_module: inginious.frontend.plugins.contests

A new configuration page named Contest appears on the administration page. To enable the contest mode, check the Enable contest plugin box on the appropriate course. Please note that the plugin will override the task accessibility dates.

Simple grader plugin

This simple grader allows anonymous POST requests without storing submissions in database.

To enable the plugin, add to your configuration file:

plugins:
    - plugin_module: inginious.frontend.plugins.simple_grader
      courseid : "external"
      page_pattern: "/external"
      return_fields: "^(result|text|problems)$"
  • courseid is the course id you want to expose to the simple grader.
  • page_pattern is the URL at which you want to make the simple grader available.
  • return_fields is a regular expression matching the submission fields that can be returned via the simple grader.

A demonstration POST form will be available at the page_pattern specified URL.

New synchronized job

External submissions must take the form of a POST request on the url defined by page_pattern. This POST must contains two data field:

  • taskid: the task id of the task
  • input: the input for the task, in JSON. The input is a dictionary filled with problemid:problem_answer pairs.

The return value will contains the standard return fields of an INGInious inginious.backend job plus a “status” field that will contain “ok”.

If an internal error occurs, it will return a dictionary containing

{
    "status": "error",
    "status_message": "A message containing a simple description of the error"
}
New asynchronous job

This POST request allows new jobs to be treated asynchronously. It must contains three data fields:

  • taskid: the task id of the task
  • input: the input for the task, in JSON. The input is a dictionary filled with problemid:problem_answer pairs.
  • async: field that indicate that the job must be launched asynchronously. Only have to be present, content is not read.

The return value will be a dictionnary containing:

{
    "status": "done",
    "jobid": "the jobid of the async job. Will be needed to get the results."
}

or

{
    "status": "error",
    "status_message": "A message describing the error"
}
Get status of asynchronous job

Given a jobid in input (as field of the POST request) and will return either:

{
    "status": "waiting"
}

or

{
    "status": "error",
    "status_message": "A message describing the error"
}

or

{
    "status": "done",
    "...":"..."
}

where ... are the results of the job, as defined in the return_fields configuration value.

Git Repo plugin

This plugin allows saving submissions history in a Git repository, according to the following path pattern : courseid/taskid/username. The version kept in the head of branch is the latest submission made.

To enable this plugin, add to your configuration file:

plugins:
    - plugin_module: inginious.frontend.plugins.git_repo
      repo_directory: "./repo_submissions"

The repo_directory parameter specify the path to the repository that must be initialized before configuration.

JSON task file readers plugin

It is possible to store task files in other formats than YAML. However, these plugins are provided for retro-compatibility with previous supported formats, which are deprecated. You therefore use these plugins at your own risks.

To enable the JSON task file format:

plugins:
    - plugin_module: inginious.frontend.plugins.task_file_readers.json_reader

Troubleshooting common problems

Solving problems hangs, on OS X with docker-machine or VirtualBox to run Docker

There is a known problem with VirtualBox shared folders: it is impossible for the VM to create a unix socket inside (for strange reasons). To solve this, you can mount instead your /Users directory using docker-machine-nfs:

LTI grades are not pushed back using LetsEncrypt

INGInious uses PyLTI which uses oauth, oauth2 and libhttp2. The list of certificate authorities known to libhttp2 may be out of day with your host operating system. In particular, as of August 2016, it does not include the LetsEncrypt CA, and thus websites protected with a LetsEncrypt certificate won’t work (you won’t be able to push grades back).

LTI frontend keeps on OAuth errors

LTI uses OAuth which uses time-based replay prevention. You need to insure that your webserver (LTI consumer) and LTI producer have reasonably synchronous clocks.

Impossible to get the LTI frontend work

You may find `http://ltiapps.net/test/tp.php`_ and `http://ltiapps.net/test/t.php`_ useful when debugging producers and consumers.

Commands reference

Frontend commands

inginious-install

Assistant to create a complete configuration file for the INGInious frontend.

Read and follow screen instructions.

If you want to configure the frontend by hand, see Configuration reference for a list of entries.

inginious-install [-h] [--file FILE]
--file

Specify the configuration file to use. By default, it is configuration.yaml or configuration.lti.yaml, depending on which backend you use

-h, --help

Display the help message.

inginious-webapp

Start the Web App Frontend. This command can run a standalone web server (see --host and --port options), but also as a FastCGI backend.

inginious-webapp [-h] [--config CONFIG] [--host HOST] [--port PORT]
--config

Specify the configuration file to use. By default, it is configuration.yaml or configuration.json, depending on which is found first. This can also be specified via the INGINIOUS_WEBAPP_CONFIG environment variable.

--host HOST

Specify the host to which to bind to. By default, it is localhost. This can also be specified via the INGINIOUS_WEBAPP_HOST environment variable.

--port PORT

Specify the port to which to bind to. By default, it is 8080. This can also be specified via the INGINIOUS_WEBAPP_PORT environment variable.

-h, --help

Display the help message.

Backend commands

inginious-agent-docker

Start a Docker grading agent. This is typically only used when running the INGInious backend remotely. If you configured INGInious to use a local backend, it is automatically run by inginious-webapp or inginious-lti.

inginious-agent-docker [-h] [--debug-host DEBUG_HOST]
                       [--debug-ports DEBUG_PORTS] [--tmpdir TMPDIR]
                       [--tasks TASKS] [--concurrency CONCURRENCY] [-v]
                       backend
-h, --help

Display the help message.

--debug-host DEBUG_HOST

The agent hostname for SSH debug. If not specified, the agent autodiscover the public IP address.

--debug-ports DEBUG_PORTS

Range of port for job remote debugging. By default it is 64120-64130

--tmpdir TMPDIR

Path to a directory where the agent can store information, such as caches. Defaults to ./agent_data

--tasks TASKS

The path to the directory containing the courses. Default to ./tasks.

--concurrency CONCURRENCY

Maximal number of jobs that can run concurrently on this agent. By default, it is the two times the number of cores available.

-v, --verbose

Increase output verbosity: logging level to DEBUG.

backend

The backend port, using the following syntax : protocol://host:port. E.g. tcp://127.0.0.1:2001. The agent will connect to the backend listening on that port.

inginious-agent-mcq

Start a MCQ grading agent. This is typically only used when running the INGInious backend remotely. If you configured INGInious to use a local backend, it is automatically run by inginious-webapp or inginious-lti.

inginious-agent-mcq [-h] [--tasks TASKS] [-v] backend
-h, --help

Display the help message.

--tasks TASKS

The path to the directory containing the courses. Default to ./tasks.

-v, --verbose

Increase output verbosity: logging level to DEBUG.

backend

The backend port, using the following syntax : protocol://host:port. E.g. tcp://127.0.0.1:2001. The agent will connect to the backend listening on that port.

inginious-backend

Start an INGInious backend. This is typically only used when running the INGInious backend remotely. If you configured INGInious to use a local backend, it is automatically run by inginious-webapp or inginious-lti.

inginious-backend [-h] [-v] agent client
-h, --help

Display the help message.

-v, --verbose

Increase output verbosity: logging level to DEBUG.

agent

The agents port, using the following syntax : protocol://host:port. E.g. tcp://127.0.0.1:2001. The backend will listen for grading agents on that port.

client

The clients port, using the following syntax : protocol://host:port. E.g. tcp://127.0.0.1:2000. The backend will listen for client frontend on that port.

Utilities

inginious-synchronize

Synchronization tool for INGInious Git repos. Each repository is suppposed to content the files required for a course. When run, the tool pulls the modifications done remotely and force-merge the the local version if conflicts cannot be resolved automatically.

A configuration file synchronize.json must be provided or specified using environment variable INGINIOUS_SYNC_CONFIG. This file contains the main task directory as well as the course identifier, private key for pulls and repo url, as follows:

{
    "maindir":"../tasks",
    "repos":
    [
        {
            "course":"TEST0000",
            "keyfile":"TEST0000.key",
            "url":"git@github.com:user/TEST0000.git"
        },
        {
            "course":"TEST0001",
            "keyfile":"TEST0001.key",
            "url":"git@github.com:user/TEST0001.git"
        }
    ]
}

For more compatibility, please run this command in an ssh-agent session.

Before adding in crontab, add the following lines to .ssh/config for user who runs the scripts :

Host *
    StrictHostKeyChecking no

This tells SSH not to check host keys, we always trust the remote servers

inginious-containers-update

Update all the containers created and/or maintained by the INGInious team.

Takes no argument, but needs a properly configured Docker environment. If you use a remote Docker instance, please check the DOCKER_HOST environment variable.

inginious-database-update

Update the database to use it with the latest INGInious version.

-h, --help

Display the help message.

-c, --config

Specify the INGInious config file to use. If not specified, looks for a configuration file in the current directory

Teacher’s documentation

Contents:

What is INGInious?

INGInious provides a simple and secure way to execute and test untrusted code. It has been developed by the INGI department (Université catholique de Louvain) to automatic grading of programming assignments. The whole tool is written in Python (version 3.5+) and relies on Docker to provide secure execution environments and on MongoDB to keep track of submissions.

INGInious is completely language-agnostic and is able to run anything. Currently, this is limited to Linux programs as only Linux containers are provided and supported.

INGInious also provides an LTI module, allowing its integration to your existing (Open) edX, Moodle,… courses.

How does INGInious work?

INGInious is based on the concept of tasks (see Creating a new task). A task is a set of one or more related (sub)questions. For each task, an infinite number of submissions is allowed, but a user must wait for the result of its current submission before trying a new one.

For simplicity, tasks are grouped by courses (see Creating a new course). Usually, an INGInious course has one task per assignment.

A submission is a set of deliverables (chunks of code, files, archives, etc.) that correspond each to one of the (sub)questions of the task. These files are made available to the run file (see Run file), a special script provided by the task. That script is responsible for providing feedback on the submission by compiling, executing or applying any form of checking and testing to the deliverables. In its simplest form, the feedback consists of either success or failed.

This run file is run inside a container (precisely, a grading container), that completely jails the execution of the script, because even teachers and assistants are never fully trusted. Grading containers are able to start sub-containers, called student containers, that runs the scripts that the students sent with their submission, in another jailed environment.

This separation in two step of the grading is mandatory to ensure a complete security for the server hosting INGInious and a complete security of the grading process, making impossible for the student to interact “badly” with the run script.

These containers are created/described by very simple files called Dockerfile. They allow to create containers for anything that runs on Linux. For details about to create new containers and add new languages to INGInious, see Creating a new container image.

Architecture

INGInious comes with three distinct parts, the backend (and its agent) and a frontend.

The backend (see ../dev_doc/backend) receives the code of the students and sends it to its agent (see ../dev_doc/agent), which is then responsible to send it to a Docker container, and interact with the request made by the container.

That container then makes some verifications on the submission and returns one of the following four possible status : success, crash, timeout, or failed.

INGInious also provides a frontend (see ../dev_doc/frontend). Made with MongoDB as database, the frontend is in fact an extension of the backend and allows students to work directly on a website. This frontend also provides statistics and management tools for the teachers.

Most of these functionalities can be extended through plugins.

For a more advanced view of the architecture of INGInious, see Understand INGInious.

Docker containers

Docker containers are small virtual operating systems that provides isolation between the processes and resources of the host operating system. Docker allow to create and ship any software on any free Linux distribution.

As there are no hypervisor, the processes launched in the container are in fact directly run by the host operating system, which allows applications to be amazingly fast.

Docker allows teachers to build new containers easily, to add new dependencies to the tests applied on the student’s code (see Creating a new container image)

Isolation

Isolation allows teachers and system administrators to stop worrying about the code that the students provides.

For example, if a student provides a forkbomb instead of a good code for the test, the forkbomb will be contained inside the container. The host operating system (the computer that runs INGInious) won’t be affected.

The same thing occurs with memory consumption and disk flood. The running time of a code is also limited.

Compatibility

INGInious provides two compatibility layers with Pythia v0 and v1. Except the task description file which has to be updated, everything is 100% compatible with INGInious.

Creating a new course

Courses are defined by subdirectories found in the tasks directory, which has been specified in the configuration. See Configuration reference. These subdirectories are composed of a course.yaml file describing the course parameters and other subdirectories corresponding to tasks (See Creating a new task).

Here is an example of the content of a tasks folder:

tasks/
    course_id_1/
        course.yaml
        task_id_1/
            task.yaml
            run
            ...
        ...
    ...

Ideally, you should only give permissions to a course folder to the course administrator if needed. The webapp task editor should not require you to give this access. If needed, several methods exist. See inginious-synchronize for Git repository synchronization.

Tutorial

Creating courses is reserved to the super-administrators (See Configuration reference). Course administrators are then able to configure the course by themselves.

Note

Demonstration tasks are made available for download here. They can also be downloaded and installed automatically via the inginious-install script.

Using the webapp
  1. As a super-administrator, go to the bottom of the course list and enter a new course id, for instance demo, and click on Create new course. A newly created hidden course named demo appears on the list.
  2. Click on that course, and then on Course administration to change the course parameters, add course administrators and tasks.

Please note that, if you give access to the course directory to course administrators, you still have to do some manual work for this to be effective.

Manually

The course description is a YAML file containing all the course parameters used by INGInious. Here is a simple course description. Put this file with the name course.yaml in a newly created demo folder in your tasks directory.

name: "[DEMO] Demonstration course"
admins:
- demouser

This elementary course description file will make a new publicly visible course with id demo appear as [DEMO] Demonstration course on the course list.

Course description files

Inside the task folder, courses are identified by subdirectories name by their course id and containing a course.yaml file. For instance, this file, for a course with id courseid1, should be placed in a courseid1 subdirectory.

course.yaml is a YAML file containing the course configuration.

admins:
  - demouser
name: "[DEMO] Demonstration course"
tutors: []
groups_student_choice: false
use_classrooms: true
accessible: true
registration: true
registration_password: null
registration_ac: null

While the course.yaml file must be present at the course root dir, all the fields inside are actually only used by the webapp. Here are the possible fields to set:

  • name Displayed name of the course on the course list.

  • admins List of administrators usernames. These users will have complete administrations right on the course.

  • tutors List of tutors usernames (restricted-rights teaching assistants). These users will have read-only rights on the course content. They cannot change course parameters nor tasks, cannot replay submissions or wipe the course data. However, they can manage the classroom composition and download all the student submissions.

  • accessible When this field is defined, the course is only visible if within the defined period. A course is always accessible to its admins, and is only hidden to normal users, even if they are registered to the course. This field can contain theses values:

    true

    the task is always accessible;

    false

    the task is never accessible;

    "<start>/<end>"

    where <start> and <end> are either empty or valid dates like “2014-05-10 10:11:12” or “2014-06-18”. The task is only accessible between <start> and <end>. If one of the values is empty, the corresponding limit does not apply.

    Dates are always considered as a precise instant (to te lowest resolution of the clock). For example, “2014-05-21” is expanded to “2014-05-21 00:00:00”. This means that start limits are inclusive, while end limits are exclusive.

    Some examples:

    "2014-05-21 / 2014-05-28"
    "/ 2014-01-01 " # (strictly) before january the first
    "2030-01-01 /" # opens in 2030
    "/" # Always open
    "/ 2013-12-31 23:59:59" # closes one minute before "/ 2014-01-01"
    
  • registration When this field is defined, users can only register to the course between the defined period. It takes the same arguments as accessible.

  • allow_unregister If this field is defined and set to false, then students are not allowed to auto-unregister from the course.

  • registration_password A password that is asked upon registration to the course. If empty or not defined, no password will be asked.

  • registration_ac Access control (AC) method. Can be null (anyone can register), username (filter by username), realname (filter by real name) or email (filter by email address). If AC is activated, the allowed values for the filter should be set in the registration_ac_list key.

  • registration_ac_list If AC is activated, registration_ac_list should contain a list of values for the filter.

  • nofrontend If this field is defined and set to true, then the course won’t be displayed on the webapp course list.

  • groups_student_choice If this field is defined and set to true and if collaborative work is activated for a given task, students will be invited to register by themselves for a group or team before submitting.

  • use_classrooms If this field is set to true, the classroom model will be used, otherwise, the team model will be used. The default value for this field is true. (See Classrooms and teams)

Creating a new task

Tutorial

In this document we will describe how to create a simple task, that checks that a code in Python returns “Hello World!”.

Note

Demonstration tasks are made available for download here. They can also be downloaded and installed automatically via the inginious-install script.

Creating the task description
Using the webapp

If you are using the webapp, this procedure can be done using the graphical interface:

  1. Go to the Course administration/Tasks page, enter helloworld as a new task id and click on Create new task.
  2. In the Basic settings tab, set the task name to Hello World! and put some context and author name. Container setup can be left with default parameters.
  3. In the Subproblems tab, add a new code-type problem with problem id question1.
  4. Set some problem name and context, and set language to python.
  5. Save changes and go to Task files tab.
Manually

This is only possible if the administrator has given access to the course directory to the course administrator.

The task description is a YAML file describing everything that INGInious needs to know to verify the input of the student. Here is a simple task description. Put this file with the name task.yaml in a newly created helloworld folder in your course directory.

author: "The INGInious authors"
accessible: true
name: "Hello World!"
context: "In this task, you will have to write a python script that displays 'Hello World!'."
problems:
    question1:
        name: "Let's print it"
        header: "def func():"
        type: "code"
        language: "python"
limits:
    time: 10
    memory: 50
    output: 1000
environment: default

Most of the fields are self-explanatory. Some remarks:

  • The field problems is a dictionary of problems. Each problem must have an unique id, for example here question1.
  • Problem question1 have its type field that equals to code, which means the student must enter some code to answer the question. Other types exists, such as multiple-choice.
  • The field limits are the limits that the task cannot exceed. The time is in seconds, and memory and output are in MB.
  • The environment field is intended to change the environment where the tasks run. The available environments are those you downloaded during installation or those you created by creating a grading container. Please see Creating a new container image.

More documentation is available here: Creating a new task.

Creating the run file

In your task folder, you will put every file needed to test the input of the student. This folder content can be shown in the webapp in the Task files tab of the Edit task page.

  1. Create a template file template.py, where we will put the code of the student.

    def func():
        @    @question1@@
    
        func()
    

    The syntax is very simple: put a first @ on the line where you want to put the code of the student. Then indent the line and write a second @. Now write the problem id of the problem you want to take the input from (question1) then write another @, write a possible suffix (not used here), and then finish the line with a last @.

  2. Create the run file. This file will be the script that is launched when the task is started. Here we will create a bash script, that parses the template and verifies its content.

    #! /bin/bash
    
    # This line parses the template and put the result in studentcode.py
    parsetemplate --output student/studentcode.py template.py
    
    # Verify the output of the code...
    output=$(run_student python student/studentcode.py)
    if [ "$output" = "Hello World!" ]; then
        # The student succeeded
        feedback-result success
        feedback-msg -m "You solved this difficult task!"
    else
        # The student failed
        feedback-result failed
        feedback-msg -m "Your output is $output"
    fi
    

    Here we use three commands provided by INGInious, parsetemplate, run_student and feedback. The code is self-explanatory; just notice the usage of run_student that ask INGInious (precisely the Docker agent) to start a new student container and run inside the command python studentcode.py.

    Please note that the run_student command is fully configurable: you can change the environment on which you run the task, define new timeouts, memory limits, … See run_student for more details.

  3. If not using the webapp, don’t forget to give the run file the execution rights:

    $ chmod +x helloworld/run
    

More documentation is available here: Run file.

Run file

When the student have submit his/her code, INGInious starts a new Docker container with the right environment for the task (as given in the .task file). Inside this container is launched a script, called run, that you have to provide in the directory of your task.

Here is a simple example of a run file, compatible with the default environment, that simply returns that the student’s code is OK:

#!/bin/bash
feedback-result success

The run script is simply an executable application (a bash script, a python script, or a compiled executable runnable by the container). INGInious’ default containers provides commands (also available as python libraries) to interact with the backend.

By default, the script is run inside the container in the /task directory, by a non-root user. You can modify the container to change this (and everything else).

Feedback commands
feedback-result

The feedback-result command sets the submission result of a task, or a problem, and uses the following syntax :

feedback-result [-i|--id PROBLEM_ID] RESULT

The execution result can be of different types:

  • success : the student succeeded the task
  • failed : there are error in the student answer
  • timeout : the tests timed out
  • overflow :there was a memory/disk overflow
  • crash : the tests crashed

For instance, the following command will inform that the student succeeded:

feedback-result success

In Python : the equivalent command can be directly obtained with:

from inginious import feedback
feedback.set_global_result("success") # Set global result to success
feedback.set_problem_result("failed", "q1") # Set 'q1' subproblem result to failed
feedback-grade

The feedback-grade command sets the submission grade and uses the following syntax:

feedback-grade GRADE

If no grade is specified, the result score will be binary. This means that a failed submission will give a 0.0% score to the student, while a successful submission will give a 100.0% score to the student. For instance, the following command will give an 87.8% grade to the student:

feedback-grade 87.8

In Python : the equivalent command can be directly obtained with:

from inginious import feedback
feedback.set_grade(87.8) # Set the grade to 87.8%
feedback-msg-tpl

The feedback-msg-tpl sets the feedback message associated to the task or a subproblem, using a Jinja2 <http://jinja.pocoo.org/docs/2.9/> template.

It needs the name of a template. The command attempt to use a translated version of the template first; given that you give TPLNAME as first argument to the command, feedback-msg-tpl will attempt to find the template, by search in this order:

  • [local_dir]/TPLNAME.XX_XX.tpl
  • [task_dir]/lang/XX_XX/TPLNAME.tpl (preferred way)
  • [local_dir]/TPLNAME.tpl

Once found, the template is parsed using Jinja2 <http://jinja.pocoo.org/docs/2.9/>, which allows you to send parameters to the template. These parameters should be given in the command line, in the form name=value:

feedback-msg-tpl TPLNAME option1=value1 option2=value2

Inside your template, you can use these parameters like this:

Option 1 was {{ option1 }} and the option 2 was {{ option 2 }}

Which will return

Option 1 was value1 and the option 2 was value2

See the Jinja2 documentation to discover all possibilities.

Your template must return a valid RestructuredText.

Optional parameters:

-a, --append append to current feedback, if not specified, replace the current feedback.
-i, --id PROBLEM_ID
 problem id to which associate the feedback, leave empty for the whole task.
feedback-msg

The feedback-msg command sets the feedback message associated to the task or a subproblem. It has several optional parameters:

-a, --append append to current feedback, if not specified, replace the current feedback.
-i, --id PROBLEM_ID
 problem id to which associate the feedback, leave empty for the whole task.
-e, --escape interprets backslash escapes
-m, --message MESSAGE
 feedback message

If the message is not specified, the feedback message is read from stdin. For instance, the command can be used as follows:

feedback-msg -ae -m "This is the correct answer.\n\nWell done!"

In Python : the equivalent command can be directly obtained with:

from inginious import feedback
feedback.set_global_feedback("Well done !") # Set global feedback text to `Well done !`
feedback.set_problem_feedback("This is not correct.", "q1") # Set 'q1' problem feedback to `This is not correct.`
feedback-custom

The feedback-custom command sets a pair of key/value custom feedback, mainly used with plugins, and uses the following syntax :

feedback-custom [-j|--json] key value

The --json parameter indicates if value must be parsed as a JSON string. Please refer to the plugin documentation to know which value you have to set for key and value parameters.

For instance, the following command set the value 56 to the score key:

feedback-custom score 56

In Python : the equivalent command can be directly obtained with:

from inginious import feedback
feedback.set_custom_value("score", 56) # Set the `score` key to value 56
tag-set

The tag-set command sets the value of the tag specified by the tag identifier to True or False. It uses the following syntax:

tag-set tag value

For instance, the following command set the value of the my_tag tag to True:

tag-set my_tag true

In Python : the equivalent command can be directly obtained with:

from inginious import feedback
feedback.feedback.set_tag("my_tag", True) # Sets the skill/misconception tag as True
tag

The tag command defines a new unexpected tag to appear in the submission feedback. It uses the followig syntax:

tag value

For instance, the following command defines a new A new tag tag that will appear in the submission feedback:

tag "A new tag"

In Python : the equivalent command can be directly obtained with:

from inginious import feedback
feedback.tag("A new tag") # Sets a new unexpected tag
reStructuredText helper commands

Several helper commands are available to format the feedback text, which format is reStructuredText.

rst-code

The rst-code command generates a code-block with the specified code snippet and language to enable syntax highlighting. It has the following parameters:

-l, --language LANGUAGE
 snippet language, leave empty to disable syntax highlighting
-e, --escape interprets backslash escapes
-c, --code CODE
 snippet code

If the code parameter is not specified, it is read on standard input. The result is written on standard output. For instance, the command can be used as follows:

cat test.java | rst-code -l java | feedback-msg -a

In Python : the equivalent command can be directly obtained with:

from inginious import rst
codeblock = rst.get_codeblock("java", "int a = 42;") # Java codeblock with `int a = 42;` code
feedback.set_global_feedback(codeblock, True) # Appends the codeblock to the global feedback
rst-image

The rst-image command generates a raw reStructuredText block containing the image to display. It has the following syntax

rst-image [-f|--format FORMAT] FILEPATH

The optional format parameter is used to specify the image format (jpg, png,…) if this is not explicitly specified the the image filename. The output is written on the standard output. For instance, the command can be used as follows:

rst-image generated.png | feedback -a

In Python : the equivalent command can be directly obtained with:

from inginious import rst
imgblock = rst.get_imageblock("smiley.png") # RST block with image
feedback.set_global_feedback(imgblock, True) # Appends the image block to the global feedback
rst-msgblock

The rst-msgblock command is used to generate a reStructuredText admonition in a specific colour according to the message type. It has the following optional parameters:

-c, --class CSS_CLASS
 

Bootstrap alert CSS class:

  • success
  • info
  • warning
  • danger
-e, --escape interprets backslash escapes
-t, --title TITLE
 message title
-m, --message MESSAGE
 message text

If the message parameter is not set, the message is read from standard input. For instance, the command can be used as follows:

rst-msgblock -c info -m "This is a note" | feedback -ae

In Python : the equivalent command can be directly obtained with:

from inginious import rst
admonition = rst.get_admonition("success", "Yeah!", "Well done!") # RST message block of class "success" and title "Yeah!"
feedback.set_global_feedback(admonition, True) # Appends the block to the global feedback
rst-indent

The rst-indent command is used to handle the indentation of the given text. It has the following optional arguments:

-e, --escape interprets backslash escapes
-c, --indent-char INDENT_CHAR
 indentation char, default = tabulation
-a, --amount AMOUNT
 amount of indentation, default = 1
-m, --message MESSAGE
 message text

If the message parameter is not set, the text is read from standard input. The amount of indentation can be negative to de-indent the text. For instance, the command can be used as follows, to add an image to the feedback, inside a list item, for instance :

rst-image generated.png | rst-indent | feedback -a

In Python : the equivalent command can be directly obtained with:

from inginious import rst
rawhtml = rst.indent_block(1, "<p>A paragraph!</p>", "\t") # Indent the HTML code with 1 unit of tabulations
feedback.set_global_feedback(".. raw::\n\n" + rawhtml, True) # Appends the block to the global feedback
Input commands
getinput

The getinput command returns the input given by the student for a specific problem id. For example, for the problem id “pid”, the command to run is:

getinput pid

When a problem is defined with several boxes, the argument becomes pid/bid where “pid” stands for the problem id and “bid” for “box id”. If the problem is a file upload, the problem id can be appended with :filename or :value to retrieve its filename or value.

Note that getinput can also retrieve the username/group of the user that submitted the task. You simply have to run

getinput @username

If the submission is made as a user, it will contain the username. It it’s made as a group, it will contain the list of the user’s usernames in the group, joined with ‘,’.

The four letter code of the student’s language (for example en_US or fr_FR) can also be retrieved using

getinput @lang

Note that plugins are free to add new @-prefixed fields to the available input using the new_submission hook.

In Python : the equivalent command can be directly obtained with:

from inginious import input
thecode = input.get_input("q1") # Fetch the code for problem `q1`
parsetemplate

The parsetemplate command injects the input given by the student in a template. The command has this form:

parsetemplate [-o|--output outputfile] template

where template is the file to parse. Output file is the destination file. If the -o option is not given, the template will be replaced.

The markup in the templates is very simple: @prefix@problemid@suffix@. Prefix allows to correct the indentation when needed (this is useful in Python).

Example of template file (in java)

public class Main
{
    public static void main(String[] args)
    {
@        @problem_one@@
    }
}

To access the filename and text content of a submitted file, the problemid can be followed by a :filename or :value suffix.

In Python : the equivalent command can be directly obtained with:

from inginious import input
thecode = input.parse_template("student.c") # Parse the `student.c` template file
thecode = input.parse_template("template.c", "student.c") # Parse the `template.c` template file and save the parsed file into `student.c`
run_student

run_student allows the run file to start, at will, sub-containers. This makes you able to secure the grading, making sure the untrusted code made by the student don’t interact with yours.

The sub-container is launched with a different user who has read-write accesses to the task student subdirectory. Only the changes made in that directory will remain in the main container.

run_student is fully configurable; you can change the container image (environment), set new timeouts, new memory limits, … And you can call it as many time as you want.

--container CONTAINER
 Name of the container to use. The default is the same as the current container.
--time TIME Timeout (in CPU time) for the container. The default is the same as the current container.
--hard-time TIME
 Hard timeout for the container (in real time). The default is three times the value indicated for –time.
--memory MEMORY
 Maximum memory for the container, in Megabytes. The default is the same as the current container.
--share-network
 Share the network stack of the grading container with the student container. This is not the case by default. If the container container has network access, this will also be the case for the student!

Beyond these optionals args, run_student also takes an additional (mandatory) arguments: the command to be run in the new container.

More technically, please note that:

  • run_student proxies stdin, stdout, stderr, most signals and the return value
  • There are special return values:
    • 252 means that the command was killed due to an out-of-memory
    • 253 means that the command timed out
    • 254 means that an error occurred while running the proxy
archive

archive allows you to put some data in an archive that will be returned to the frontend and stored in the database for future reading. You can put there debug data, for example.

The command takes some arguments, which are all optional:

-o, --outsubdir DIRECTORY
 will put the file (specified with -a or -r)in the specified sub-directory in the output archive
-a, --add FILEPATH
 add the file to the archive
-r, --remove FILEPATH
 remove the file from the archive

Task description files

Inside a course folder (see Creating a new course), tasks are identified by subdirectories named by their task id and containing a task.yaml file. For instance, this file, for a task with id taskid1, should be placed in a taskid1 subdirectory.

task.yaml is a YAML file containing information about the task.

author: Your name
context: |-
    The context of this task. Explain here what the students have to do.
order: 1
groups: false
name: The complete name of this task
accessible: true
problems:
    a_problem_id:
        name: The title of this question
        header: A header for this question
        type: code
        language: c
limits
    time: 30
    memory: 128
environment: default
network_grading: False
  • author, context, order, name, language and header are only needed if you use the frontend. context and header are parsed using restructuredText.

  • order is an integer, used by the frontend to sort the task list. Task are sorted in increasing value of order.

  • weight is a decimal value indicating the weight of the task score to use to compute the total course score.

  • accessible describes when the task is accessible to student. This field is not mandatory (by default, the task is visible) and can contain the following values:

    true

    the task is always accessible

    false

    the task is never accessible

    "START"

    where START is a valid date, like “2014-05-10 10:11:12”, or “2014-06-18”. The task is only accessible after START.

    "/END"

    where END is a valid date, like “2014-05-10 10:11:12”, or “2014-06-18”. The task is only accessible before END.

    "START/END"

    where START and END are valid dates, like “2014-05-10 10:11:12”, or “2014-06-18”. The task is only accessible between START and END.

  • problems describes sub-problems of this task. This field is mandatory and must contain at least one problem. Problem types are described in the following section Problem types. Each problem must have an id which is alphanumeric and unique.

  • limits contains the limits that will be applied on the grading container. time is the CPU timeout in seconds, and hard_time is the timeout in real time.

    By default, hard_time is defined to be to 3*``time``. This can leads to problems when INGInious is under heavy load, but allow to detect processes that do too much system interruptions (sleep calls or IO)

    memory is the maximum memory allowed to the container.

    Please note that the limits of the student containers (container that you start inside the grading container) will use these limits by default.

  • environment is the name of the Docker container in which the grading code will run. This field is only needed if there is code to correct; a multiple-choice question does not need it. This environment will be used by default for the student containers.

  • groups allows to indicate if the submission is to be done individually or per groups/teams. (see Classrooms and Teams).

  • network_grading indicates if the grading container should have access to the net. This is not the case by default.

  • evaluate indicates the submission that must be used for evaluation. This can be either:

    best

    This is the default value. In this case, the best submission is used.

    last

    In this case, the last submission is used.

    student

    In this case, the student can select the submission for evaluation. This allows student to select the submission for evaluation without submitting it again or if submission replays are planned. This feature is not available in the LTI module due to LTI specifications limitations, and will be considered as best submission.

  • submission_limit indicates the amount of submissions a student can make within a certain period of time. It is composed of two fields:

    • amount is an integer value indicating the amount of submission. A value of -1 corresponds to an infinite amount of submissions.
    • period is an integer value indicating the length of the submission period in hours. A value of -1 corresponds to an infinite period. At the end of this period, the student can submit amount submissions again during period hours.
  • stored_submissions indicates the amount of submissions that must be saved in the submission history. A value of 0 keeps all the submissions.

Problem types
Code problems

type: code problems allows students to submit their code. The code is then sent to a container where a script made by the teaching team corrects it.

Here is a simple example for a code problem

type: code
language: c
header: |-
    Hello dear student!
    I'm a multiline header!
name: A name
optional: false

header and language are only needed when using the frontend and are not mandatory. This description typically displays on the frontend a box where student can put their code.

optional is an optional field, that defaults to false, that indicates if this problem is mandatory or not.

Code problem input’s are available in the run script (see Run file) directly with the id of the problem.

Single code line problems

type: code_single_line is simply a code box that allows a single line as input.

type: code_single_line
language: c
header: |-
    Hello dear student!
    I'm another multiline header, parsed with *RST*!
name: Another problem
optional: false

Single line code problem input’s are available in the run script (see Run file) directly with the id of the problem.

Advanced code problem

Advanced code problems are available:

type: code
header: some text
name: And again, another name
boxes:
    boxId1:
        type: text
        content: Some additional text
    boxId2:
        type: input-text
        maxChars: 10
        optional: true
    boxId3:
        type: multiline
        maxChars: 1000
        lines: 8
        language: java

Boxes are displayable (on the frontend) input fields that allows the student to fill more than one entry per problem. Different box types are available, all of them are demonstrated above. Every configuration in the boxes (maxChars,*lines*,*language*) is not mandatory, except content if the box type is text, and the field optional (default to false), that indicates if the box is mandatory or not.

In the run file (see Run file), boxes input are available with the name problem_id/box_id

Match problems

Match problem are input that allows a single-line input from the student and that returns if the student entered exactly the text given in the “answer” field.

name: The answer
type: match
header: some text describing this problem
answer: 42

Match problem input’s are available in the run script (see Run file) directly with the id of the problem.

Multiple choice problems
name: An exercice
type: multiple_choice
header: The answer to life, the universe and any other things is
multiple: true
limit: 2
error_message: "Wrong answer. Don't panic, and read Hitchhiker's Guide to the Galaxy."
success_message: "You're right! But don't forget to always take your towel with you."
choices:
  - text: It is, of course, 42!
    valid: true
  - text: It should be *42*
    valid: true
  - text: 43!
    feedback: "43 isn't the answer. Maybe can you try to substract one?"
  - text: 41?
    feedback: "41 isn't the answer. Maybe can you try to add one?"

Choices are described in the choices section of the YAML. Each choice must have a text field (on the frontend) that will be parsed in restructuredText. Valid choices must have a valid: true field. The field feedback is a message that will be displayed when the student check the choice.

multiple indicates if the student may (or not) select more than one response.

Choices are chosen randomly in the list. If the limit field is set, the number of choices taken equals to the limit. There is always a valid answer in the chosen choices.

error_message and success_message are messages that will be displayed on error/success. They are parsed in RST and are not mandatory.

Multiple choice problem input’s are available in the run script (see Run file) directly with the id of the problem. The input can be either an array of integer if multiple is true or an integer. Choices are numbered sequentially from 0.

Allowing students to download files

Files stored in the subdirectory “public” of a task are available to download from the frontend.

For example, if you have a course with course id “courseA”, and a task with task id “taskB”, and if you want to distribute an image named “flower.png”, you will have to put it inside the folder “/path/to/INGInious/tasks/courseA/taskB/public/”.

The image will then be available with the url: http://domain.name.com/course/courseA/taskB/flower.png

This allows you to insert images inside the context of your tasks, and to share additional resources like datasets or slides.

Danger

The files stored in your public folder will be available to all users, without authentication needed.

Share common files between tasks

Files stored in the subdirectory “$common” of a course are available to all tasks, inside the container, at the path “/course/common”. This directory is only mounted in the grading container.

Files stored in the subdirectory “$common/student” of a course are available to all tasks, inside the container, at the path “/course/common/student”. This directory is mounted in the grading container and in student containers.

All these folder are read-only inside a container.

Random tasks parameters

Typical INGInious tasks are displayed the same way for all the students. However, it is possible to display some parameters randomly for students to avoid copy/pasting their peers submissions. This can, for instance, be a numerical value for a computation, or the name of a variable.

INGInious allows you to generate one or several random numbers that are stored in database per student. To enable this, specify the number of random numbers (of parameters) you need in the task editor Basic settings tab. By default, the same set of random numbers is kept per student. If you want to generate random numbers each time the student opens the task, check the Regenerate random input box.

Accessing the task random parameters

The random numbers generated for the task are accessible through the webapp as well as in the INGInious container and run file.

Through the webapp

Several context-specific inputs are accessible through a input Javascript dictionary in the task and subproblems context.

The @random key contains the random number list. In order to use it, declare a ..raw:: html directive in your contexts and include some Javascript code via the HTML <script> tag.

Through the container

The random number list can be accessed using the getinput API with the specific id @random.

Using the shell API:

Using the Python API:

Overriding system files

For some particular tasks (that involve networking), it may be necessary to overwrite/append to some system files, such as /etc/hosts and /etc/resolv.conf. In order to do this, create a folder systemfiles in your task, and create these files according to what you want to do:

systemfiles/hosts the content of this file is append to the /etc/hosts of the container at start. systemfiles/resolv.conf the content of /etc/resolv.conf is replaced by the content of this file if it exists.

These files are also used in containers created by run_student.It is even possible to write dynamically on systemfiles/xxx while the grading script is running, and new changes (made before run_student) will be set into the new container.

The grading container can also write directly on its /etc/hosts and /etc/resolv.conf, but it is discouradged as it will not be reflected in student containers.

Debugging tasks

There are different ways to get more insight on what’s going wrong with your tasks in case of errors. Generally, those errors come from the fact the running environment may be different from your task development environment (OS configuration, software set,…).

Debug information

Every time an administrator submit a new job and receives result on the frontend, a Debug information is made available in the sidebar. Those information contain all the submission metadata (input, result, feedback,…) as well as the grading container standard output and standard error output.

Please note that every manipulation done with those streams will not be visible anymore in those information. Redirected output won’t be shown. This is important as spawning processes in non-shell oriented languages will not redirect the spawned process standard output on the grading container standard output.

SSH debug

Debugging tasks is made more easy using SSH debug feature. This aims at providing the same user experience as local development. To make this feature work remotely (regarding the INGInious Docker agent), please make sure you’ve correctly set up the debug hosts and ports (see Configuration reference if needed).

Every administrator is able from the frontend to launch a debugging job. This is done by clicking on the >_ (left-chevron, underscore) button next to the Submit button. According to your configuration, either a SSH command-line with auto-generated password will be given you (you will, in this case, need an SSH client installed), or an embedded SSH console will pop up as the feedback position.

Best practices

Use YAML with RST

YAML with RST is the recommended way to create task descriptions. Old methods such as JSON+RST, JSON+HTML and RST+RST are deprecated and were dropped. If you really want to use them, these parsing methods are still available as plugins.

Provide a test set for the student to test their code themselves, and grade the code with at least this set

This will allow the students to make sure that they submit properly. If a test set that is public works on the student’s computer but does not work on INGInious, this is the sign of a greater problem. Give them a complete feedback on these public tests, but hide the rest to keep the difficulty of the task.

Do not use the “file” subproblem type

When you ask student to produce some code, you should always put a “code” subproblem, where the student can copy-paste his code. INGInious is made to receive text as input, no (huge) binary files.

Using Jar, Zip, Tar+Gz and other formats to convey code in binary format is most of the time convenient for the task writer, but it’s not the case for the students; tools for compressing files are not always consistent among all OS, and even inside the same OS, leading to errors that could be easily avoided using text input.

It is also prone to other types of errors: when submitting a project with a zip file or a jar, task writers ask most of the time to resubmit the complete project, with other files that may seem uneeded by the grader and that was maybe modified by the student. There is then two possibilities:

  • either the task writer effectively uses these files, leading to an error that is not really due to the student
  • or the task writer does not use them, and there is a waste of disk space

These “common files” should be located not in the code submitted by the student, but directly in the task folder.

Using “code” inputs forces you to only ask the students to submit what is really needed, improving the quality of the tasks. Zip, Jar and other archive formats should only be used when grading huge projects.

If you still have to use a “file” subproblem type, make it lightweight

For example, when submitting a Jar file (type of archive for languages that uses the JVM), please do not ask student to add common libraries (such as Scala) in their submission. These libraries can be installed in the task folder, or even better, directly in the container.

INGInious reloads most of the time the whole submission when it needs to read something inside, and reloading a file of 20M can take time.

Students should only have to upload files they modified, and not files common to all submissions.

If you still have to use a lightweight “file” subproblem, ask for code, not for binary executables

Asking for binary executable (or JVM bytecode, etc.) is a bad practice from a general point of view: * You cannot read the code of the students to check the grading * Small differences in libs used on the system and in the container can lead to errors. Even in the JVM. * It will force the grader to recompile the code of the student, allowing to make more checks

You should always ask only the code (no binary executable, no JVM .class file, …) and always recompile everything.

Do not delete tasks

Instead of deleting a task, it is better to make it unavailable, using the accessible field.

For example, if 2014-05-11 is the date where the task became unavailable:

{
        ...
        "accessible": "/2014-05-11",
        ...
}
Do not put the student’s code directly in your tests

Inserting student’s code directly in your tests is dangerous, because the student could make syntax errors that would display the code of your tests.

It is better to put all the student’s functions in a single template file, which one you will import in you test files.

Use student container

To be completely secure, run anything you do not trust inside a separate student container. Students may want to interfere with the normal work of your grading script to get better grades…

Make small tests

Inside your tasks, do not test everything in a single test file. Using a file for each test is a good practice, and will allow you to debug your code efficiently, while providing students fine-grained error descriptions.

Try to use unit-test libraries provided by your programming language.

Translating tasks

INGInious provides support for translating tasks in the language of your student. Most users will want to use the feedback-msg-tpl command, which is the most straighforward way to do translation.

Another possible method is to use gettext. This is by default supported in Python script and feedback templates, but you can use gettext in any language.

Four steps are necessary to translate a task:

  • Mark strings as translatable
  • Extract strings to create a .po file
  • Translate the extracted strings
  • Compile the translated strings into a .mo file

Marking strings in Python scripts

Simply import inginious.lang and run the command inginious.lang.init(). This will install the function _() that can be used to mark strings as translatable.

print(_("Hello"))

Marking strings in feedback templates

The function _() in always available in feedback templates:

{{ _("Hello") }}

Extracting strings

Now you need to extract the strings, for this we use babel. If it’s not already done, install babel:

pip3 install babel

Create a file named “mapping.babel”, which contains the babel mapping <http://babel.pocoo.org/en/latest/messages.html#extraction-method-mapping-and-configuration>. Here is an example of mapping that will extract both strings marked in Python and in feedback (Jinja2) templates:

[python: **.py]
[jinja2: **.tpl]
encoding = utf-8

If you use other languages, you may want to add the needed options in this file. Refer to the babel documentation.

Now, simply run the following command, which will creates a messages.pot file in your current directory:

pybabel extract -o messages.pot -F mapping.babel .

This messages.pot contains a very simple representation of the strings to translate. Here is an example of what you should obtain after running the command:

#: test.tpl:1
msgid "Hello"
msgstr ""

This will be the basis for all your translations. Let us put the file in the right place, by creating the correct directory structure:

pybabel init -i messages.pot -d lang -l fr_FR

Replace fr_FR by the language your are translating to. This will create a file lang/fr_FR/LC_MESSAGES/message.po.

Translating strings

In this file, simply change the msgstr entries with the translation of the immediately above msgid. For example, to translate the previous exemple
in french

(fr_FR):

#: test.tpl:1
msgid "Hello"
msgstr "Bonjour"

Compile your .po file into a .mo

The final step is to compile your text-based .po file into a binary .mo file, which ensures that translation occurs smoothly.

pybabel compile -d lang

Which will update all your translations.

I need more help with gettext

Gettext is a widely used tool; you will find a lot of software-independent help on the net :-)

Creating a new container image

Creating the Dockerfile

Container images can be viewed as small operating systems with specific software and configuration. The main force of the container images is that they are very simple to create and modify, using Dockerfiles.

Here is an example of Dockerfile:

# DOCKER-VERSION 1.1.0

# Inherit from the default container, which have all the needed script to launch tasks
FROM    ingi/inginious-c-base

# Set the environment name for tasks
LABEL   org.inginious.grading.name="php"

# Add php
RUN     yum -y install php-cli

As easily seen, this Dockerfile creates a new container image that can launch PHP scripts. The syntax of these Dockerfiles is extensively described on the website of Docker, but we will detail here the most important things to know.

Each Dockerfile used on INGInious should begin with `FROM inginious/ingi-c-base` and `LABEL org.inginious.grading.name="some_name"`. The first line indicates that you take as base for your new image the default image provided with INGInious. This default image is itself based on CentOS 7, and uses yum (rpm) as package manager. It is already provided with Python and basic commands, and with all the files needed by INGInious to work. The second line is used to indicate the environment name (here, `some_name`) that will be used for tasks.

The line `RUN yum -y install php-cli` indicates to Docker that it must run the command `yum -y install php-cli` inside the image. The `yum` command is the equivalent of `apt-get` (that is the package manager for Debian, Ubuntu and derivates), but for Linux distributions that derivates from Fedora, like CentOS. This will install the package `php-cli`. Creating new containers mainly consists on adding new packages to the default container, so it is probable that your Dockerfile will contain mostly this type of lines.

Here is a little more advanced Dockerfile, that is used to provide Mozart/Oz in INGInious:

FROM    ingi/inginious-c-base
LABEL   org.inginious.grading.name="oz"

ADD mozart2.rpm /mozart.rpm
RUN yum -y install emacs tcl tk
RUN rpm -ivh /mozart.rpm
RUN rm /mozart.rpm

Again, it inherits from `ingi/inginious-c-base` and the environment name is set to `oz`. Then, it uses the command `ADD`, that takes a file in the current directory of the Dockerfile (here, `mozart2.rpm`) and copy it inside the container image, here at the path `/mozart.rpm`. It then uses three `RUN` commands to install the dependencies of Mozart, then install Mozart itself, and then removing the now uneeded rpm.

Dockerfiles can do many more things, read the documentation on the Docker website to know more about the possibilities.

“Compiling” the Dockerfile

Once you have Docker up and running, it is very simple to create a container image from a Dockerfile:

$ cd /path/to/your/dockerfile
$ docker build -t my_container_image ./

Docker will then launch a container and run the Dockerfile on it, then will save the state of the disk, that is, in fact, the container image. INGInious will automatically detect the environment based on the labels you’ve set in the Dockerfile. Therefore, the tag `my_container_image` can be set to any value. As a convention, we adopted `inginious-c-XXX`.

For the new environment to be available, you have to restart INGInious (or, at least, the Docker agent if you are running INGInious components on separate machines).

You can also enter directly in the container image to test it in the command line:

$ docker run -t -i --rm my_container_image /bin/bash

Share what you created

If you created a Dockerfile for INGInious, feel free to make a pull request in the repository associated: https://github.com/UCL-INGI/INGInious-containers

Course administration

As a course administrator, you can simply access its management page by clicking on “Course administration” in the main course page.

Students submissions

Statistics over students submissions are largely available in INGinious, and all the files related to them are stored and can be downloaded.

General overview

The administration page gives you several global list views :

  • All the tasks of a course, with the number of students who viewed the at least one time, who tried and the number of tries, as well as the number of students who succeded the task. This view is the first displayed when you click on “Manage” to enter the administration.
  • All the students/groups of a course, with the number of tasks tried and done, as well as its global progression for students. This view can be accessed by switching to “Students”/”Groups” in the main administration page.
  • All the students/groups who tried a given task, if they succeded it, and the number of submissions they did. You can show these information by clicking “View results” on the main administration page or by clicking “Statistics” on the task page.
  • All the tasks tried by a given student/group, if (s)he/they succeded it and the number of submissions (s)he/they did. These information can be displayed by clicking “View” in the student/group list of a course.
  • All the submissions made by a student/group for a given tasks, with date of submission and the global result. Submissions can be displayed by clicking “View submissions” in tasks lists.

All the tables can be downloaded in CSV format to make some further treatment in your favourite spreadsheet editor.

More information about groups possibilities can be found below.

Downloading submissions

Student submissions can be downloaded from the Download submissions and statistics pages or the submission inspection page. You are able to only download the set of evaluation submissions (according to the task parameters) or all the submissions.

Submissions are downloadable as gzip tarball (.tgz) files. You may need some third-party software if your operating system does not support this format natively. The files contain, for each submissions, a test file with extension test containing the all the submission metadata and the associated archive folder containing all the files that have been exported using the archive API (See Run file).

Replaying submissions

Student submissions can be replayed either from the Replay submissions and statistics pages or the submission inspection page. Different replay scheme are available:

  • As replacement of the current student submission result. This is the default scheme for the Replay submissions page. When replayed, submission input are put back in the grading queue. When the job is completed, the newly computed result will replace the old one. This is useful if you want to change the grading scripts during or after the assignment period and want all students to be graded the same way. You can replay only the evaluation submission or all submissions. However, please note that if replayed, the best submission can be replaced by an older best submission.
  • As a personal copy. This mode is only available from the submission inspection page and copy the student input to generate a new personal copy. This is useful for debugging if a problem occur with a specific student submission. Submission copy is also available with SSH debug mode.

Warning

This feature is currently under testing. As the same job queue is used for standard submissions and submission replay, it is recommended not to launch huge replay jobs on a very active INGInious instance.

Task edition

All tasks can be edited from the webapp. To access the task editor, just click on Edit task on the task page or from the main administration page.

Task problems containing boxes are not graphically editable due to their high modularity. These kinds of problem editable on-line in YAML format.

Adding/removing problems

Adding and removing problems are very easy in the task editor, go to the end of the page or click on the quick link “Add subproblem”. You’ll then be brought to a new form asking a problem-id (alphanumerical characters) and a problem type.

To make a more complex question with boxes, choose “custom” problem and write the YAML problem description as described in the task file format.

When editing a multiple choice problem, you’re asked if the student is shown a multiple-answers- or single-answer-problem and which of the possible choices is (are) good answer(s).

Task files

Task files can be created, uploaded and modified from the task edition page. Only text-base files can be edited from the webapp. Binary files can however be uploaded.

The behaviour of the Move action is Unix-like : it can be used for renaming files.

Classrooms and teams

Collaborative work and separate students administration are possible in INGInious. Two models are available:

  • Classrooms and groups : Classrooms are useful to administratively separate students following the same course. They offer separate statistics to help the teacher identify problems students may encounter in this particular context.

    Submissions groups can be set in classrooms and define a set of users that will submit together. Their submissions will contain as authors all the students that were members of the group at submission time. Note that students cannot collaborate with students from another classroom. In this case, please consider using only teams, as described below.

  • Teams : Teams are administratively-separated submissions groups. They are internally assimilated to classrooms with a unique submission group. They offer separate statistics for each submission group.

Choice between these two models can be made in the course settings. Switching from one model to another will reinitialize the all course structure (that is, students registration also). Course structures can be backed up if necessary from the classrooms/teams administration pages.

Creation and edition

Classrooms and teams are created and edited from the web app in the course administration.

Classrooms and groups

In the classroom list view, specify a classroom description, and click on “Create new classroom”. The newly created classroom will appear in the list.

To edit a classroom, click on the quick link “Edit classroom” located on the right side of the table. You’ll be able to change the classroom description, the associated teaching staff, and to specify the (grouped) students. Assigning tutors will help them to retrieve their classroom statistics.

The student list is entirely managed by drag-and-drop. You can create a new group on the same page, set its maximum size, and drag-and-drop ungrouped students or already grouped students in the newly created group.

Teams

To create a new team, click on “Edit teams” simply in the team list view and press on the “New team” button. You’ll then be able to specify the team description, its maximum size, assigned tutors and students. Team edition works the same way.

The student list is entirely managed by drag-and-drop. Students can be moved from one team to another by simply moving his name to the new team.

Group/team attribution

If you do not really matter the way students work together, you can set empty groups or teams with maximum size and let the students choose their groups or teams themselves. Just check the option in the course settings to allow them to gather. When submissions will be retrieved, the group/team members will be displayed as the authors as with staff-defined groups or teams.

Course structure upload

You can generate the course classroom or team structure with an external tool and then upload it on INGInious. This is done with a YAML file, which structure for classrooms or teams are similar and described below. The course structure can be upload on the classroom or team list view in the course administration.

Classrooms YAML structure
-    description: Classroom 1
     tutors:
             - tutor1
             - tutor2
     students:
             - user1
             - user2
     groups:
             - size: 2
               students:
                     - user1
                     - user2
-    description: Classroom 2
     tutors:
             - tutor1
             - tutor2
     students:
             - user3
             - user4
  • description is a string and corresponds to your class description
  • tutors is a list of strings representing the usernames of the assigned classroom tutors.
  • students is a list of strings representing the usernames of the classroom students.
  • groups is a list of group structures containing the following elements :
    • size: the maximum group size
    • students: the list of student usernames in this group
Teams YAML structure
-    description: Team 1
     tutors:
             - tutor1
             - tutor2
     students:
             - user1
             - user2
-    description: Team 2
     tutors:
             - tutor1
             - tutor2
     students:
             - user3
             - user4
  • description is a string and corresponds to your team description
  • tutors is a list of strings representing the usernames of the assigned team tutors.
  • students is a list of strings representing the usernames of the team students.
Backup course structure

Course structures (classrooms or teams) can be exported for backup or manual edition via the classroom/team list page in the course administration pages. Simply click on the “Download structure” button. The download file will have the same format as described above.

Using through LTI (edX, Moodle, …)

INGInious implements the LTI specification in order to integrate into edX, Moodle, or any other LMS that also implements this specification. To get started, all you need is to activate te LTI mode in the course administration and define your LTI keys (consumer key and secret). You’ll then be able to use INGInious tasks as activities in your LMS.

Defining LTI keys

The LTI keys are defined in the course administration by first activating the LTI mode. Then, add consumer keys and secrets separated by a colon in the LTI keys field. For instance, here’s an example of a set of keys and secrets:

consumer_key_1:a_very_secret_password
consumer_key_2:wow_such_secret

This obviously defines two LTI keys, consumer_key_1 and consumer_key_2, with passwords a_very_secret_password and wow_such_secret. You also need to check the Send grades back option to actually send the scores from INGInious to your LTI.

Setting up your LMS

Setting up (Open) edX

edX provides a good tutorial on how to install LTI components.

When it asks for the LTI passport, you have to enter it in the format an_id_that_you_define:consumer_key:password. A good example, taking values from the start of this document, would be

inginious:consumer_key_1:a_very_secret_password

The launch url is, if your server is located at https://HOST:PORT/, and you want to load the task task_id from the course course_id:

https://HOST:PORT/lti/course_id/task_id

Please note that, for now, official edX needs https. You also need to set the LTI activity to accept a score back from INGInious if your have set up INGInious such that scores are sent back.

Setting up Moodle

Under edition mode, select add an activity, choose external tool, and confirm.

Directly click on Show more.... Fill in the activity name.

The Launch URL is, if your server is located at https://HOST:PORT/, and you want to load the task task_id from the course course_id:

https://HOST:PORT/lti/course_id/task_id

For the field Launch Container, the best value is “Embded without block”. Consumer key and Consumer secret are the LTI key you defined earlier. In the Privacy fieldset, check that accept grades from the tool is checked. Leave the other fields blank (or modify them as you want).

Save, and it should work.

Setting up other LMS

INGInious has only been tested with edX and Moodle, but it should work out-of-the-box with any LMS that respects LTI 1.1. You are on your own for the configuration, though; but with the LTI keys and the launch URL, it should be enough to configure anything.

Developer’s documentation

Understand INGInious

INGInious is made from three different packages:

  • The common which contains basic blocks, like tasks and courses. Derivates from this blocks are created by the frontend and other modules. The common does not need the backend nor the frontend;
  • The agent, that runs jobs. It interacts directly with Docker to start new containers, and sends the grades back to the backend. A specific part of the backend is in charge of starting the agents automatically; you most of time won’t need to it manually. The agent needs to be run on the Docker host, as it interacts with other containers with Unix sockets, and must also interact with CGroups to allow a very fine management of timeouts and memory limits.
  • The backend, which is in charge of handling grading requests, giving the work to distant agents; the backend is made to be simple and frontend-agnostic; you can ‘easily’ replace the frontend by something else. The backend only store information about running tasks. This point is important when considering replication and horizontal scalability (see later)
  • The frontend which is a web interface for the backend. It provides a simple yet powerful interface for students and teachers. It is made to be “stateless”: all its state is stored in DB, allowing to replicate the frontend horizontally.

Basic architecture of INGInious

The following schema shows the basic architecture of INGInious:

_images/inginious_arch.png

Scalability of Docker hosts

In order to share the work between multiple servers, INGInious can use multiple agents, as shown in the following schema. The completely horizontal scalability is (nearly) without additional configuration, and can be made fully automatic with a bit of work.

_images/inginious_arch_docker.png

Scalability of the INGInious frontend

As the backend only stores information about running submission, and the frontend is stateless, we can use the replication feature of MongoDB to scale horizontally the frontends too. The (final) schema below shows the most advanced way of configuring INGInious, with multiple frontends replicated and multiple Docker hosts.

_images/inginious_arch_full.png

Grading containers and student containers

A grading container is a container that do the grading. It typically runs a script made by a teacher or its assistants, a launch sub-containers, called student containers, that will separately jail code made by students.

A single grading container can launch more than one student container; the interaction between the two is completely secured by the agent.

Jobs

When you send a student’s input to the backend, it creates what we call a job. Jobs are sent to an object called the Client, which itself is a simple communication layer to a job queue that we call the Backend. The Backend itself can be used by multiple Clients, and dispatch jobs among Agents, which can be of different types (for now, we have two kinds of agents, DockerAgent and MCQAgent)

When a job is submitted, a callback must be given: it is automatically called when the task is done, asynchronously.

Submission

A submission is an extension of the concept of job. A submission only exists in the frontend, and it is mainly a job saved to db.

Creating plugins

INGInious provides a simple plugin system that allow to register some hooks to extend existing features, create new frontend pages and features, and add new authentication methods.

Hooks actually call callback functions that you indicated with the add_hook method from HookManager. Please note that all hooks may be called by another thread, so all actions done into a hook have to be thread-safe.

Tutorial

The following code adds a new page displaying This is a simple demo plugin on the /plugindemo location.

class DemoPage(object):
    """ A simple demo page showing how to add a new page """

    def GET(self):
        """ GET request """
        return "This is a simple demo plugin"


def init(plugin_manager, course_factory, client, plugin_config):
    """ Init the plugin """
    plugin_manager.add_page("/plugindemo", DemoPage)

The plugin is initialized by the plugin manager, which is the frontend-extended hook manager, by calling method init. This method takes four arguments:

  • plugin_manager which is the plugin manager singleton object. The detailed API is available at inginious.frontend.common.plugin_manager. Please note that PluginManager inherits from inginious.common.hook_manager module.

  • course_factory which is the course factory singleton object, giving you abstraction to the tasks folder. The detailed API is available at inginious.common.course_factory module.

  • client which is the INGInious client singleton object, giving you access to the backend features, as launching a new job. The detailed API is available at inginious.client.client module.

  • plugin_config which is a dictionary containing the plugin configuration fields set in your configuration.yaml file. For instance, configuration:

    plugins:
        - plugin_module: inginious.frontend.plugins.demo
          param1: "value1"
    

    will generate the following plugin_config dictionary :

    {"plugin_module": "inginious.frontend.plugins.demo", "param1": "value1"}
    

The remaining INGInious classes can be used from your plugins using correct imports. The init method gives you access to the different singletons used by INGInious which are instantiated at boot time. For instance, LTIPage class can be used as base for a new LTI page.

The plugin_module configuration parameter corresponds to the Python package in which the init method is found. A demonstration plugin is found in the inginious.frontend.plugins.demo. You do not need to include your plugin in the INGInious sources. As long as your plugin is found in the Python path, it will remain usable by INGInious.

List of hooks

You may be interested to generate some actions useful for your plugins before or after some INGInious events. You would therefore need to add a hook method. This can be done using the add_hook method of package inginious.frontend.common.plugin_manager. For instance, the following plugin :

import logging

def submission_done(submission, archive, newsub):
    logging.getLogger("inginious.frontend.plugins.demo").info("Submission " + str(submission['_id']) + " done.")

def init(plugin_manager, course_factory, client, plugin_config):
    """ Init the plugin """
    plugin_manager.add_hook("submission_done", submission_done)

will log each submission id that has been returning from the backend.

Each hook available in INGInious is described here, starting with its name and parameters. Please refer to the complete inginious.frontend.common package documentation for more information on the data returned by those hooks.

css

Returns : List of path to CSS files.

Used to add CSS files in the header. Should return the path to a CSS file (relative to the root of INGInious).

course_admin_menu (course)

course : inginious.frontend.common.courses.FrontendCourse

Returns : Tuple (link, name) or None.

Used to add links to the administration menu. This hook should return a tuple (link,name) where link is the relative link from the index of the course administration. You can also return None.

main_menu (template_helper)

template_helper : inginious.frontend.common.template_helper.TemplateHelper

Returns : HTML or None.

Allows to add HTML to the menu displayed on the main (course list) page. template_helper is an object of type TemplateHelper, that can be useful to render templates.

course_menu (course, template_helper)

course : inginious.frontend.common.courses.FrontendCourse

template_helper : inginious.frontend.common.template_helper.TemplateHelper

Returns : HTML or None.

Allows to add HTML to the menu displayed on the course page. Course is the course object related to the page. template_helper is an object of type TemplateHelper, that can be useful to render templates.

task_menu (course, task, template_helper)

course : inginious.frontend.common.courses.FrontendCourse

task : inginious.frontend.common.tasks.FrontendTask

template_helper : inginious.frontend.common.template_helper.TemplateHelper

Returns: HTML or None.

Allows to add HTML to the menu displayed on the course page. course is the course object related to the page. task is the task object related to the page. template_helper is an object of type TemplateHelper, that can be useful to render templates.

welcome_text (template_helper)

template_helper : inginious.frontend.common.template_helper.TemplateHelper

Returns : HTML or None.

Allows to add HTML to the login/welcome page. template_helper is an object of type TemplateHelper, that can be useful to render templates.

javascript_header

Returns : List of path to Javascript files.

Used to add Javascript files in the header. Should return the path to a Javascript file (relative to the root of INGInious).

javascript_footer

Returns : List of path to Javascript files.

Used to add Javascript files in the footer. Should return the path to a Javascript file (relative to the root of INGInious).

course_accessibility (course, default)

Returns: inginious.frontend.accessible_time.AccessibleTime

course : inginious.common.courses.Course

default : Default value as specified in the configuration

Overrides the course accessibility.

task_accessibility (course, taskid, default)

Returns: inginious.frontend.accessible_time.AccessibleTime

course : inginious.common.courses.Course

task : inginious.common.tasks.Task

default : Default value as specified in the configuration

Overrides the task accessibility

task_limits (course, taskid, default)

Returns: Task limits dictionary

course : inginious.common.courses.Course

task : inginious.common.tasks.Task

default : Default value as specified in the configuration

Overrides the task limits

task_context (course, taskid, default)

Returns: inginious.frontend.common.parsable_text.ParsableText

course : inginious.common.courses.Course

task : inginious.common.tasks.Task

default : Default value as specified in the configuration

Overrides the task context

task_network_grading (course, taskid, default)

Returns: True or False

course : inginious.common.courses.Course

task : inginious.common.tasks.Task

default : Default value as specified in the configuration

Overrides the task network-enable option

new_submission (submission, inputdata)

submissionid : ObjectId corresponding to the submission recently saved in database.

submission : Dictionary containing the submission metadata without input field.

inputdata : Dictionary containing the raw input data entered by the student. Each key corresponding to the problem id.

Called when a new submission is received. Please note that the job is not yet send to the backend when this hook is called, pay also attention that a submission is the name given to a job that was made through the frontend. It implies that jobs created by plugins will not call new_submission nor submission_done.

submission_done (submission, archive, newsub)

submission : Dictionary containing the submission metadata.

archive : Bytes containing the archive file generated by the job execution. This can be None if no archive is generated (for einstance, in MCQ).

newsub : Boolean indicating if the submission is a new one or a replay.

Called when a submission has ended. The submissionid is contained in the dictionary submission, under the field _id.

template_helper ()

Returns : Tuple (name,func)

Adds a new helper to the instance of TemplateHelper. Should return a tuple (name,func) where name is the name that will be indicated when calling the TemplateHelper.call method, and func is the function that will be called.

feedback_text (task, submission, text)

Returns : {“task”: task, “submission”: submission, “text”: modified_text}

Modifies the feedback to be displayed. This hook is called each time a submission is displayed. You have to return the origin task and submission objects in the return value. text is in HTML format.

feedback_script (task, submission)

Return : javascript as an str.

Javascript returned by this hook will be executed by the distant web browser when the submission is loaded. This hook is called each time a submission is displayed. Pay attention to output correct javascript, as it may break the webpage.

task_editor_tab (course, taskid, task_data, template_helper)

course : inginious.frontend.courses.WebAppCourse

task_data : OrderedDict

template_helper : inginious.frontend.template_helper.TemplateHelper

This hook allows to add additional tabs on the task editor.

course is the course object related to task, task_data is the task descriptor content and template_helper is an object of type TemplateHelper, that can be useful to render templates such as tab content.

task_editor_submit (course, taskid, task_data, task_fs)

course : inginious.frontend.courses.WebAppCourse

task_data : OrderedDict

task_fs : inginious.common.filesystems.local.LocalFSProvider

This hook allows to process form data located in the added tabs.

course is the course object related to task, task_data is the task descriptor content and task_fs is an object of type LocalFSProvider.

Additional subproblems

Additional subproblems can be defined and added via plugins. A basic example is available on GitHub repo UCL-INGI/INGInious-problems-demo.

Subproblems are defined at both the backend and frontend side. At the backend side, it consists of a class inheriting from inginious.common.tasks_problems.Problem and implementing the following abstract methods:

  • get_type(cls) returning an alphanumerical string representing the problem type.

  • input_is_consistent(self, task_input, default_allowed_extension, default_max_size returning True if the task_input dictionary provided by the INGInious client is consistent and correct for the agent.

  • input_type(self) returning str, dict or list according to the actual data sent to the agent.

  • check_answer(self, task_input, language) returning a tuple whose items are:

    1. either True, False or None, indicating respectively that the answer is valid, invalid, or need to be sent to VM
    2. the second is the error message assigned to the task, if any (unused for now)
    3. the third is the error message assigned to this problem, if any
    4. the fourth is the number of errors.

    This method should be called via a compatible agent, as for MCQs. The Docker agent will not call this method. task_input is the dictionary provided by the INGInious client after its consistency was checked. language is the gettext 2-letter language code.

  • get_text_fields(cls) returns a dictionary whose keys are the problem YAML fields that require translation and values are always True.

  • parse_problem(self, problem_content) returns the modified problem_content` returned by the INGInious studio. For instance, strings-encoded int values can be cast to int here.

At the frontend side, it consists of a class inheriting from inginious.frontend.tasks_problems.DisplayableProblem and implementing th following abstract methods:

  • get_type_name(self, gettext) returning a human-readable transleted string representing the problem type. gettext is the frontend user-associated gettext function.
  • get_renderer(cls, template_helper) returning the template renderer used for the subproblem. template_helper is the webapp TemplateHelper singleton. It can be used to specify a local template folder.
  • show_input(self, template_helper, language, seed) returning a HTML code displayed after the subproblem context to the student. template_helper is the webapp TemplateHelper singleton. language` is the gettext 2-letter language code. seed is a seed to be used in the random number generator. For simplicity, it should be a string and the usage of the username is recommended, as the seed is made to ensure that a user always see the same exercise. Classes inheriting from DisplayableProblem should prepend/append a salt to the seed and then create a new instance of Random from it. See inginious.frontend.tasks_problems.DisplayableMultipleChoiceProblem for an example.
  • show_editbox(cls, template_helper, key) returning a HTML code corresponding to the subproblem edition box. template_helper is the webapp TemplateHelper singleton. key is the problem type sent by the frontend.

How to extend INGInious

Creating a new frontend

INGInious is mainly a backend that is agnostic. It can be used to run nearly everything. The backend’s code is in backend. You must use these classes to run new jobs.

The common contains classes that are intended to be inherited by new “frontends”. The frontend given with INGInious is in fact an (big) extension of the common module. You can use it as an example on how to extend INGInious.

Code documentation

inginious package

inginious.get_root_path()[source]

Returns the INGInious root path

Subpackages
inginious.agent package
class inginious.agent.Agent(context, backend_addr, friendly_name, concurrency, tasks_filesystem)[source]

Bases: object

An INGInious agent, that grades specific kinds of jobs, and interacts with a Backend.

environments
Returns:a dict of available environments (containers most of the time) in the form {
”name”: { #for example, “default”
”id”: “container img id”, # “sha256:715c5cb5575cdb2641956e42af4a53e69edf763ce701006b2c6e0f4f39b68dd3” “created”: 12345678 # create date, as an unix timestamp

}

}

If the environments are not containers, fills created with a fixed date (that will be shared by all agents of the same version), that could be 0. id can be anything, but should also be the same for the same versions of environments.

Only the name field is shared with the Clients.

kill_job(message: inginious.common.messages.BackendKillJob)[source]
new_job(message: inginious.common.messages.BackendNewJob)[source]

Starts a new job. Most of the time, this function should not call send_job_result directly (as job are intended to be asynchronous). When there is a problem starting the job, raise CannotCreateJobException. If the job ends immediately, you are free to call send_job_result. :param message: message containing all the data needed to start the job :return: nothing. If any problems occurs, this method should raise a CannotCreateJobException,

which will result in the cancellation of the job.
run()[source]

Runs the agent. Answer to the requests made by the Backend. May raise an asyncio.CancelledError, in which case the agent should clean itself and restart completely.

send_job_result(job_id: Tuple[bytes, str], result: str, text: str = '', grade: float = None, problems: Dict[str, Tuple[str, str]] = None, tests: Dict[str, Any] = None, custom: Dict[str, Any] = None, archive: Optional[bytes] = None, stdout: Optional[str] = None, stderr: Optional[str] = None)[source]

Send the result of a job back to the backend. Must be called once and only once for each job :exception JobNotRunningException: is raised when send_job_result is called more than once for a given job_id

send_ssh_job_info(job_id: Tuple[bytes, str], host: str, port: int, key: str)[source]

Send info about the SSH debug connection to the backend/client. Must be called at most once for each job. :exception JobNotRunningException: is raised when the job is not running anymore (send_job_result already called) :exception TooManyCallsException: is raised when this function has been called more than once

exception inginious.agent.CannotCreateJobException(message)[source]

Bases: Exception

Exception that should be raised when a (batch or std) job cannot be created in Agent.new_job or Agent.new_batch_job

exception inginious.agent.JobNotRunningException[source]

Bases: Exception

Exception raised by the Agent when the functions send_job_result/send_ssh_job_info are called but the job is not running anymore

exception inginious.agent.TooManyCallsException[source]

Bases: Exception

Exception raised by the Agent when the function send_ssh_job_info has been called more than once

Submodules
inginious.agent.docker_agent module
class inginious.agent.docker_agent.DockerAgent(context, backend_addr, friendly_name, concurrency, tasks_fs: inginious.common.filesystems.provider.FileSystemProvider, ssh_host=None, ssh_ports=None, tmp_dir='./agent_tmp')[source]

Bases: inginious.agent.Agent

create_student_container(job_id, parent_container_id, sockets_path, student_path, systemfiles_path, course_common_student_path, socket_id, environment_name, memory_limit, time_limit, hard_time_limit, share_network, write_stream)[source]

Creates a new student container. :param write_stream: stream on which to write the return value of the container (with a correctly formatted msgpack message)

environments
Returns:a dict of available environments (containers most of the time) in the form
{
“name”: { #for example, “default”
“id”: “container img id”, # “sha256:715c5cb5575cdb2641956e42af4a53e69edf763ce701006b2c6e0f4f39b68dd3” “created”: 12345678 # create date, as an unix timestamp

}

}

If the environments are not containers, fills created with a fixed date (that will be shared by all agents of the same version), that could be 0. id can be anything, but should also be the same for the same versions of environments.

Only the name field is shared with the Clients.

handle_job_closing(container_id, retval)[source]

Handle a closing student container. Do some cleaning, verify memory limits, timeouts, … and returns data to the backend

handle_running_container(job_id, container_id, inputdata, debug, ssh_port, orig_env, orig_memory_limit, orig_time_limit, orig_hard_time_limit, sockets_path, student_path, systemfiles_path, course_common_student_path, future_results)[source]

Talk with a container. Sends the initial input. Allows to start student containers

handle_student_job_closing(container_id, retval)[source]

Handle a closing student container. Do some cleaning, verify memory limits, timeouts, … and returns data to the associated grading container

kill_job(message: inginious.common.messages.BackendKillJob)[source]

Handles kill messages. Kill things.

new_job(message: inginious.common.messages.BackendNewJob)[source]

Handles a new job: starts the grading container

run()[source]

Runs the agent. Answer to the requests made by the Backend. May raise an asyncio.CancelledError, in which case the agent should clean itself and restart completely.

inginious.agent.mcq_agent module
class inginious.agent.mcq_agent.MCQAgent(context, backend_addr, friendly_name, concurrency, tasks_filesystem)[source]

Bases: inginious.agent.Agent

environments
Returns:a dict of available environments (containers most of the time) in the form
{
“name”: { #for example, “default”
“id”: “container img id”, # “sha256:715c5cb5575cdb2641956e42af4a53e69edf763ce701006b2c6e0f4f39b68dd3” “created”: 12345678 # create date, as an unix timestamp

}

}

If the environments are not containers, fills created with a fixed date (that will be shared by all agents of the same version), that could be 0. id can be anything, but should also be the same for the same versions of environments.

Only the name field is shared with the Clients.

kill_job(message: inginious.common.messages.BackendKillJob)[source]
new_job(msg: inginious.common.messages.BackendNewJob)[source]

Starts a new job. Most of the time, this function should not call send_job_result directly (as job are intended to be asynchronous). When there is a problem starting the job, raise CannotCreateJobException. If the job ends immediately, you are free to call send_job_result. :param message: message containing all the data needed to start the job :return: nothing. If any problems occurs, this method should raise a CannotCreateJobException,

which will result in the cancellation of the job.
inginious.backend package
Submodules
inginious.backend.backend module
class inginious.backend.backend.Backend(context, agent_addr, client_addr)[source]

Bases: object

Backend. Central point of the architecture, manages communication between clients (frontends) and agents. Schedule jobs on agents.

handle_agent_hello(agent_addr, message: inginious.common.messages.AgentHello)[source]

Handle an AgentAvailable message. Add agent_addr to the list of available agents

handle_agent_job_done(agent_addr, message: inginious.common.messages.AgentJobDone)[source]

Handle an AgentJobDone message. Send the data back to the client, and start new job if needed

handle_agent_job_ssh_debug(_, message: inginious.common.messages.AgentJobSSHDebug)[source]

Handle an AgentJobSSHDebug message. Send the data back to the client

handle_agent_job_started(agent_addr, message: inginious.common.messages.AgentJobStarted)[source]

Handle an AgentJobStarted message. Send the data back to the client

handle_agent_message(agent_addr, message)[source]

Dispatch messages received from agents to the right handlers

handle_client_get_queue(client_addr, _: inginious.common.messages.ClientGetQueue)[source]

Handles a ClientGetQueue message. Send back info about the job queue

handle_client_hello(client_addr, _: inginious.common.messages.ClientHello)[source]

Handle an ClientHello message. Send available containers to the client

handle_client_kill_job(client_addr, message: inginious.common.messages.ClientKillJob)[source]

Handle an ClientKillJob message. Remove a job from the waiting list or send the kill message to the right agent.

handle_client_message(client_addr, message)[source]

Dispatch messages received from clients to the right handlers

handle_client_new_job(client_addr, message: inginious.common.messages.ClientNewJob)[source]

Handle an ClientNewJob message. Add a job to the queue and triggers an update

handle_client_ping(client_addr, _: inginious.common.messages.Ping)[source]

Handle an Ping message. Pong the client

run()[source]
send_container_update_to_client(client_addrs)[source]
Parameters:client_addrs – list of clients to which we should send the update
update_queue()[source]

Send waiting jobs to available agents

inginious.client package
Submodules
inginious.client.client module
class inginious.client.client.AbstractClient[source]

Bases: object

close()[source]

Close the Client

get_available_containers()[source]

Return the list of available containers for grading

get_job_queue_info(jobid)[source]
Parameters:jobid – the jobid of a task.
Returns:If the submission is in the queue, then returns a tuple (nb tasks before running (or -1 if running), approx wait time in seconds) Else, returns None
get_job_queue_snapshot()[source]
Get a snapshot of the remote backend job queue. May be a cached version.
May not contain recent jobs. May return None if no snapshot is available

Return a tuple of two lists (or None, None): jobs_running: a list of tuples in the form

(job_id, is_current_client_job, info, launcher, started_at, max_end) where - job_id is a job id. It may be from another client. - is_current_client_job is a boolean indicating if the client that asked the request has started the job - agent_name is the agent name - info is “courseid/taskid” - launcher is the name of the launcher, which may be anything - started_at the time (in seconds since UNIX epoch) at which the job started - max_end the time at which the job will timeout (in seconds since UNIX epoch), or -1 if no timeout is set
jobs_waiting: a list of tuples in the form
(job_id, is_current_client_job, info, launcher, max_time) where - job_id is a job id. It may be from another client. - is_current_client_job is a boolean indicating if the client that asked the request has started the job - info is “courseid/taskid” - launcher is the name of the launcher, which may be anything - max_time the maximum time that can be used, or -1 if no timeout is set
kill_job(job_id)[source]

Kills a running job :param job_id:

new_job(task, inputdata, callback, launcher_name='Unknown', debug=False, ssh_callback=None)[source]

Add a new job. Every callback will be called once and only once.

Parameters:
  • inputdata (Storage or dict) – input from the student
  • callback (__builtin__.function or __builtin__.instancemethod) – a function that will be called asynchronously in the client’s process, with the results. it’s signature must be (result, grade, problems, tests, custom, archive), where: result is itself a tuple containing the result string and the main feedback (i.e. (‘success’, ‘You succeeded’); grade is a number between 0 and 100 indicating the grade of the users; problems is a dict of tuple, in the form {‘problemid’: result}; test is a dict of tests made in the container custom is a dict containing random things set in the container archive is either None or a bytes containing a tgz archive of files from the job
  • launcher_name (str) – for informational use
  • debug (bool or string) – Either True(outputs more info), False(default), or “ssh” (starts a remote ssh server. ssh_callback needs to be defined)
  • ssh_callback (__builtin__.function or __builtin__.instancemethod or None) – a callback function that will be called with (host, port, password), the needed credentials to connect to the remote ssh server. May be called with host, port, password being None, meaning no session was open.
Returns:

the new job id

start()[source]

Starts the Client. Should be done after a complete initialisation of the hook manager.

class inginious.client.client.Client(context, backend_addr, queue_update=10)[source]

Bases: inginious.client._zeromq_client.BetterParanoidPirateClient

close()[source]

Close the Client

get_available_containers()[source]

Return the list of available containers for grading

get_job_queue_info(jobid)[source]
get_job_queue_snapshot()[source]
kill_job(job_id)[source]

Kills a running job

new_job(task, inputdata, callback, launcher_name='Unknown', debug=False, ssh_callback=None)[source]

Add a new job. Every callback will be called once and only once.

Parameters:
  • inputdata (Storage or dict) – input from the student
  • callback (__builtin__.function or __builtin__.instancemethod) – a function that will be called asynchronously in the client’s process, with the results. it’s signature must be (result, grade, problems, tests, custom, archive), where: result is itself a tuple containing the result string and the main feedback (i.e. (‘success’, ‘You succeeded’); grade is a number between 0 and 100 indicating the grade of the users; problems is a dict of tuple, in the form {‘problemid’: result}; test is a dict of tests made in the container custom is a dict containing random things set in the container archive is either None or a bytes containing a tgz archive of files from the job
  • launcher_name (str) – for informational use
  • debug (bool or string) – Either True(outputs more info), False(default), or “ssh” (starts a remote ssh server. ssh_callback needs to be defined)
  • ssh_callback (__builtin__.function or __builtin__.instancemethod or None) – a callback function that will be called with (host, port, password), the needed credentials to connect to the remote ssh server. May be called with host, port, password being None, meaning no session was open.
Returns:

the new job id

start()[source]

Starts the Client. Should be done after a complete initialisation of the hook manager.

inginious.client.client_buffer module

Contains ClientBuffer, which creates a buffer for a Client

class inginious.client.client_buffer.ClientBuffer(client)[source]

Bases: object

A buffer for a Client

get_result(bjobid)[source]

Get the result of task. Must only be called ONCE, AFTER the task is done (after a successfull call to is_done). :return a tuple (result, grade, problems, tests, custom, archive) result is itself a tuple containing the result string and the main feedback (i.e. (‘success’, ‘You succeeded’) grade is a number between 0 and 100 indicating the grade of the users problems is a dict of tuple, in the form {‘problemid’: result} test is a dict of tests made in the container custom is a dict containing random things set in the container archive is either None or a bytes containing a tgz archive of files from the job

is_done(bjobid)[source]

Return true if the job is done

is_waiting(bjobid)[source]

Return true if the job is in queue

new_job(task, inputdata, launcher_name='Unknown', debug=False)[source]

Runs a new job. It works exactly like the Client class, instead that there is no callback

inginious.client.client_sync module

A synchronized “layer” for Client

class inginious.client.client_sync.ClientSync(client)[source]

Bases: object

Runs job synchronously

new_job(task, inputdata, launcher_name='Unknown', debug=False)[source]

Runs a new job. It works exactly like the Client class, instead that there is no callback and directly returns result, in the form of a tuple (result, grade, problems, tests, custom, archive).

inginious.common package

Common package: basic library for INGInious, needed by all its components.

Subpackages
inginious.common.task_file_readers package

Managers for handling the different format of task files

Submodules
inginious.common.task_file_readers.abstract_reader module

Task file managers

class inginious.common.task_file_readers.abstract_reader.AbstractTaskFileReader[source]

Bases: object

Manages a type of task file

dump(descriptor)[source]

Dump descriptor and returns the content that should be written to the task file

get_ext()[source]

Returns the task file extension. Must be @classmethod!

load(file_content)[source]

Parses file_content and returns a dict describing a task

inginious.common.task_file_readers.yaml_reader module

YAML task file manager

class inginious.common.task_file_readers.yaml_reader.TaskYAMLFileReader[source]

Bases: inginious.common.task_file_readers.abstract_reader.AbstractTaskFileReader

Read and write task descriptions in YAML

dump(data)[source]

Dump descriptor and returns the content that should be written to the task file

classmethod get_ext()[source]

Returns the task file extension. Must be @classmethod!

load(content)[source]

Parses file_content and returns a dict describing a task

inginious.common.tests package

Tests for the inginious.common package

Submodules
inginious.common.tests.TestBase module
class inginious.common.tests.TestBase.TestDirectoryHash[source]

Bases: object

Test all the functions that involves file hash

setUp()[source]
tearDown()[source]
test_directory_compare_from_hash()[source]
test_directory_content_with_hash()[source]
test_hash_file()[source]
class inginious.common.tests.TestBase.TestIdChecker[source]

Bases: object

Test the id checker

test_id_checker_invalid_1()[source]
test_id_checker_invalid_2()[source]
test_id_checker_invalid_3()[source]
test_id_checker_valid_1()[source]
class inginious.common.tests.TestBase.TestJSONYAMLReaderWriter[source]

Bases: object

Test the functions load_json_or_yaml and write_json_or_yaml

setUp()[source]
tearDown()[source]
test_json_read()[source]
test_json_write()[source]
test_yaml_read()[source]
test_yaml_write()[source]
inginious.common.tests.TestCourse module
class inginious.common.tests.TestCourse.TestCourse[source]

Bases: object

setUp()[source]
test_all_courses_loading()[source]

Tests if all courses are loaded by Course.get_all_courses()

test_course_loading()[source]

Tests if a course file loads correctly

test_invalid_coursename()[source]
test_tasks_loading()[source]

Tests loading tasks from the get_tasks method

test_tasks_loading_invalid()[source]
test_unreadable_course()[source]
class inginious.common.tests.TestCourse.TestCourseWrite[source]

Bases: object

Test the course update function

setUp()[source]
tearDown()[source]
test_course_update()[source]
inginious.common.tests.TestCustomYaml module
class inginious.common.tests.TestCustomYaml.TestCustomLoad[source]

Bases: object

setUp()[source]
tearDown()[source]
test_load_ordereddict()[source]
test_load_string()[source]
class inginious.common.tests.TestCustomYaml.TestCustomWrite[source]

Bases: object

setUp()[source]
tearDown()[source]
test_write_long_str()[source]
test_write_long_str_obj()[source]
test_write_ordereddict()[source]
test_write_string()[source]
inginious.common.tests.TestHookManager module
class inginious.common.tests.TestHookManager.TestHookManager[source]

Bases: object

make_exception()[source]
test_exception()[source]

Hook Manager should silently ignore hooks that make exceptions

test_multple()[source]
inginious.common.tests.TestTask module
class inginious.common.tests.TestTask.test_tasks_basic[source]

Bases: object

setUp()[source]
test_check_answer_1()[source]
test_check_answer_2()[source]
test_course()[source]
test_input_consistent_invalid()[source]
test_input_consistent_valid()[source]
test_invalid_limits_1()[source]
test_invalid_limits_2()[source]
test_no_problems()[source]
test_task_invalid()[source]
test_task_invalid_name()[source]
test_task_loading()[source]

Tests if a course file loads correctly

class inginious.common.tests.TestTask.test_tasks_problems[source]

Bases: object

setUp()[source]
test_code()[source]

Tests code problems methods

test_file()[source]

Tests file problems methods

test_match()[source]

Tests match problems methods

test_multiple_choice()[source]

Tests multiple choice problems methods

test_problem_types()[source]

Tests if problem types are correctly recognized

Submodules
inginious.common.asyncio_utils module

Utilities for asyncio

class inginious.common.asyncio_utils.AsyncIteratorWrapper(obj)[source]

Bases: object

A wrapper that converts old-style-generators to async generators using run_in_executor

class inginious.common.asyncio_utils.AsyncProxy(module, loop=None, executor=None)[source]

Bases: object

An asyncio proxy for modules and classes

sync

Return the original sync module/class

inginious.common.base module

Basic dependencies for every modules that uses INGInious

inginious.common.base.directory_compare_from_hash(from_directory, to_directory)[source]
Parameters:
  • from_directory – dict in the form {file: (hash of the file, stat of the file)} from directory_content_with_hash
  • to_directory – dict in the form {file: (hash of the file, stat of the file)} from directory_content_with_hash
Returns:

a tuple containing two list: the files that should be uploaded to “to_directory” and the files that should be removed from “to_directory”

inginious.common.base.directory_content_with_hash(directory)[source]
Parameters:directory – directory in which the function list the files
Returns:dict in the form {file: (hash of the file, stat of the file)}
inginious.common.base.get_json_or_yaml(file_path, content)[source]

Generate JSON or YAML depending on the file extension.

inginious.common.base.hash_file(fileobj)[source]
Parameters:fileobj – a file object
Returns:a hash of the file content
inginious.common.base.id_checker(id_to_test)[source]

Checks if a id is correct

inginious.common.base.id_checker_tests(id_to_test)[source]

Checks if a id is correct

inginious.common.base.load_json_or_yaml(file_path)[source]

Load JSON or YAML depending on the file extension. Returns a dict

inginious.common.base.loads_json_or_yaml(file_path, content)[source]

Load JSON or YAML depending on the file extension. Returns a dict

inginious.common.base.write_json_or_yaml(file_path, content)[source]

Write JSON or YAML depending on the file extension.

inginious.common.course_factory module

Factory for loading courses from disk

class inginious.common.course_factory.CourseFactory(filesystem: inginious.common.filesystems.provider.FileSystemProvider, task_factory, hook_manager, course_class=<class 'inginious.common.courses.Course'>)[source]

Bases: object

Load courses from disk

create_course(courseid, init_content)[source]
Parameters:
  • courseid – the course id of the course
  • init_content – initial descriptor content

:raise InvalidNameException or CourseAlreadyExistsException Create a new course folder and set initial descriptor content, folder can already exist

delete_course(courseid)[source]
Parameters:courseid – the course id of the course

:raise InvalidNameException or CourseNotFoundException Erase the content of the course folder

get_all_courses()[source]
Returns:a table containing courseid=>Course pairs
get_course(courseid)[source]
Parameters:courseid – the course id of the course

:raise InvalidNameException, CourseNotFoundException, CourseUnreadableException :return: an object representing the course, of the type given in the constructor

get_course_descriptor_content(courseid)[source]
Parameters:courseid – the course id of the course

:raise InvalidNameException, CourseNotFoundException, CourseUnreadableException :return: the content of the dict that describes the course

get_course_fs(courseid)[source]
Parameters:courseid
Returns:a FileSystemProvider pointing to the directory of the course
get_task(courseid, taskid)[source]

Shorthand for CourseFactory.get_course(courseid).get_task(taskid) :param courseid: the course id of the course :param taskid: the task id of the task :raise InvalidNameException, CourseNotFoundException, CourseUnreadableException, TaskNotFoundException, TaskUnreadableException :return: an object representing the task, of the type given in the constructor

get_task_factory()[source]
Returns:the associated task factory
update_course_descriptor_content(courseid, content)[source]

Updates the content of the dict that describes the course :param courseid: the course id of the course :param content: the new dict that replaces the old content :raise InvalidNameException, CourseNotFoundException

inginious.common.course_factory.create_factories(fs_provider, task_problem_types, hook_manager=None, course_class=<class 'inginious.common.courses.Course'>, task_class=<class 'inginious.common.tasks.Task'>)[source]

Shorthand for creating Factories :param fs_provider: A FileSystemProvider leading to the courses :param hook_manager: an Hook Manager instance. If None, a new Hook Manager is created :param course_class: :param task_class: :return: a tuple with two objects: the first being of type CourseFactory, the second of type TaskFactory

inginious.common.courses module

Contains the class Course and utility functions

class inginious.common.courses.Course(courseid, content_description, course_fs, task_factory, hook_manager)[source]

Bases: object

Represents a course

get_descriptor()[source]

Get (a copy) the description of the course

get_fs()[source]

Returns a FileSystemProvider which points to the folder of this course

get_id()[source]

Return the _id of this course

get_task(taskid)[source]

Returns a Task object

get_tasks()[source]

Get all tasks in this course

gettext(language, *args, **kwargs)[source]
inginious.common.custom_yaml module

A custom YAML based on PyYAML, that provides Ordered Dicts

inginious.common.custom_yaml.dump(data, stream=None, **kwds)[source]

Serialize a Python object into a YAML stream. If stream is None, return the produced string instead. Dict keys are produced in the order in which they appear in OrderedDicts.

Safe version.

If objects are not “conventional” objects, they will be dumped converted to string with the str() function. They will then not be recovered when loading with the load() function.

inginious.common.custom_yaml.load(stream)[source]

Parse the first YAML document in a stream and produce the corresponding Python object. Use OrderedDicts to produce dicts.

Safe version.

inginious.common.exceptions module

Some type of exceptions used by parts of INGInious

exception inginious.common.exceptions.CourseAlreadyExistsException[source]

Bases: Exception

exception inginious.common.exceptions.CourseNotFoundException[source]

Bases: Exception

exception inginious.common.exceptions.CourseUnreadableException[source]

Bases: Exception

exception inginious.common.exceptions.InvalidNameException[source]

Bases: Exception

exception inginious.common.exceptions.TaskNotFoundException[source]

Bases: Exception

exception inginious.common.exceptions.TaskReaderNotFoundException[source]

Bases: Exception

exception inginious.common.exceptions.TaskUnreadableException[source]

Bases: Exception

inginious.common.hook_manager module

Hook Manager

class inginious.common.hook_manager.HookManager[source]

Bases: object

Registers an manages hooks. Hooks are callback functions called when the inginious.backend does a specific action.

add_hook(name, callback, prio=0)[source]

Add a new hook that can be called with the call_hook function. prio is the priority. Higher priority hooks are called before lower priority ones. This function does not enforce a particular order between hooks with the same priorities.

call_hook(name, **kwargs)[source]

Call all hooks registered with this name. Returns a list of the returns values of the hooks (in the order the hooks were added)

call_hook_recursive(name, **kwargs)[source]

Call all hooks registered with this name. Each hook receives as arguments the return value of the previous hook call, or the initial params for the first hook. As such, each hook must return a dictionary with the received (eventually modified) args. Returns the modified args.

inginious.common.log module

Some common functions for logging

class inginious.common.log.CustomLogMiddleware(app, logger)[source]

Bases: object

WSGI middleware for logging the status in webpy

log(status, environ)[source]
inginious.common.log.get_course_logger(coursename)[source]
Parameters:coursename – the course id
Returns:a logger object associated to a specific course
inginious.common.log.init_logging(log_level=10)[source]

Init logging :param log_level: An integer representing the log level or a string representing one

inginious.common.message_meta module
class inginious.common.message_meta.MessageMeta(name, bases, attrs, msgtype)[source]

Bases: type

A MetaClass for messages

Provides message checking on both side of the communication.

Each class depending from this MetaClass MUST have a __init__ function that takes only arguments that are type-hinted, and that ONLY assign the argument to self, under the SAME name.

Moreover, the class should define a argument msgtype for the metaclass, that gives the name of the message when parsed

Example:

class SendNumberToContainer(metaclass=MessageMeta, msgtype=”send_nbr_container”):
def __init__(self, container_id: str, a_number: int):
self.container_id = container_id self.a_number = a_number
DEBUG = True
classmethod load(bmessage)[source]

From a bytestring given by a (distant) call to Message.dump(), retrieve the original message :param bmessage: bytestring given by a .dump() call on a message :return: the original message

class inginious.common.message_meta.ZMQUtils[source]

Bases: object

Utilities that do serializing/unserializing of messages (whose metaclass is MessageMeta)

classmethod recv(socket, skip_first=False)[source]
classmethod recv_with_addr(socket)[source]
classmethod send(socket, obj, send_white=False)[source]
classmethod send_with_addr(socket, addr: bytes, obj)[source]
inginious.common.message_meta.run_tests()[source]
inginious.common.messages module
class inginious.common.messages.AgentHello(*args, **kwargs)[source]

Bases: object

Let the agent say hello and announce which containers it has available

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.AgentJobDone(*args, **kwargs)[source]

Bases: object

Gives the result of a job. A->B.

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.AgentJobSSHDebug(*args, **kwargs)[source]

Bases: object

Gives the necessary info to SSH into a job running in ssh debug mode

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.AgentJobStarted(*args, **kwargs)[source]

Bases: object

Indicates to the backend that a job started A->B.

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.BackendGetQueue(*args, **kwargs)[source]

Bases: object

Send the status of the job queue to the client

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.BackendJobDone(*args, **kwargs)[source]

Bases: object

Gives the result of a job.

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.BackendJobSSHDebug(*args, **kwargs)[source]

Bases: object

Gives the necessary info to SSH into a job running in ssh debug mode

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.BackendJobStarted(*args, **kwargs)[source]

Bases: object

Indicates to the backend that a job started

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.BackendKillJob(*args, **kwargs)[source]

Bases: object

Kills a running job. B->A.

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.BackendNewJob(*args, **kwargs)[source]

Bases: object

Creates a new job B->A.

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.BackendUpdateContainers(*args, **kwargs)[source]

Bases: object

Update the information about the containers on the client, from the informations retrieved from the agents

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.ClientGetQueue(*args, **kwargs)[source]

Bases: object

Ask the backend to send the status of its job queue

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.ClientHello(*args, **kwargs)[source]

Bases: object

Let the client say hello to the backend (and thus register to some events)

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.ClientKillJob(*args, **kwargs)[source]

Bases: object

Kills a running job. B->A.

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.ClientNewJob(*args, **kwargs)[source]

Bases: object

Creates a new job B->A.

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.Ping(*args, **kwargs)[source]

Bases: object

Ping message

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.Pong(*args, **kwargs)[source]

Bases: object

Pong message

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
class inginious.common.messages.Unknown(*args, **kwargs)[source]

Bases: object

Unknown message. Sent by a server that do not know a specific client; probably because the server restarted

dump()
Returns:a bytestring containing a black-box representation of the message, that can be loaded using MessageMeta.load.
inginious.common.task_factory module

Factory for loading tasks from disk

class inginious.common.task_factory.TaskFactory(filesystem: inginious.common.filesystems.provider.FileSystemProvider, hook_manager, task_problem_types, task_class=<class 'inginious.common.tasks.Task'>)[source]

Bases: object

Load courses from disk

add_custom_task_file_manager(task_file_manager)[source]

Add a custom task file manager

add_problem_type(problem_type)[source]
Parameters:problem_type – Problem class
delete_all_possible_task_files(courseid, taskid)[source]

Deletes all possibles task files in directory, to allow to change the format

delete_task(courseid, taskid)[source]
Parameters:
  • courseid – the course id of the course
  • taskid – the task id of the task

:raise InvalidNameException or CourseNotFoundException Erase the content of the task folder

get_all_tasks(course)[source]
Returns:a table containing taskid=>Task pairs
get_available_task_file_extensions()[source]

Get a list of all the extensions possible for task descriptors

get_problem_types()[source]

Returns the supported problem types by this task factory

get_readable_tasks(course)[source]

Returns the list of all available tasks in a course

get_task(course, taskid)[source]
Parameters:
  • course – a Course object
  • taskid – the task id of the task

:raise InvalidNameException, TaskNotFoundException, TaskUnreadableException :return: an object representing the task, of the type given in the constructor

get_task_descriptor_content(courseid, taskid)[source]
Parameters:
  • courseid – the course id of the course
  • taskid – the task id of the task

:raise InvalidNameException, TaskNotFoundException, TaskUnreadableException :return: the content of the task descriptor, as a dict

get_task_descriptor_extension(courseid, taskid)[source]
Parameters:
  • courseid – the course id of the course
  • taskid – the task id of the task

:raise InvalidNameException, TaskNotFoundException :return: the current extension of the task descriptor

get_task_fs(courseid, taskid)[source]
Parameters:
  • courseid – the course id of the course
  • taskid – the task id of the task

:raise InvalidNameException :return: A FileSystemProvider to the folder containing the task files

update_cache_for_course(courseid)[source]

Clean/update the cache of all the tasks for a given course (id) :param courseid:

update_task_descriptor_content(courseid, taskid, content, force_extension=None)[source]

Update the task descriptor with the dict in content :param courseid: the course id of the course :param taskid: the task id of the task :param content: the content to put in the task file :param force_extension: If None, save it the same format. Else, save with the given extension :raise InvalidNameException, TaskNotFoundException, TaskUnreadableException

inginious.common.tasks module

Task

class inginious.common.tasks.Task(course, taskid, content, task_fs, hook_manager, task_problem_types)[source]

Bases: object

Contains the data for a task

allow_network_access_grading()[source]

Return True if the grading container should have access to the network

check_answer(task_input, language)[source]
Verify the answers in task_input. Returns six values 1st: True the input is currently valid. (may become invalid after running the code), False else 2nd: True if the input needs to be run in the VM, False else 3rd: Main message, as a list (that can be join with
or <br/> for example)
4th: Problem specific message, as a dictionnary (tuple of result/text) 5th: Number of subproblems that (already) contain errors. <= Number of subproblems 6th: Number of errors in MCQ problems. Not linked to the number of subproblems
get_course()[source]

Return the course that contains this task

get_course_id()[source]

Return the courseid of the course that contains this task

get_environment()[source]

Returns the environment in which the agent have to launch this task

get_fs()[source]

Returns a FileSystemProvider which points to the folder of this task

get_id()[source]

Get the id of this task

get_limits()[source]

Return the limits of this task

get_order()[source]

Get the position of this task in the course

get_problems()[source]

Get problems contained in this task

get_response_type()[source]

Returns the method used to parse the output of the task: HTML or rst

gettext(language, *args, **kwargs)[source]
input_is_consistent(task_input, default_allowed_extension, default_max_size)[source]

Check if an input for a task is consistent. Return true if this is case, false else

inginious.common.tasks_code_boxes module
inginious.common.tasks_problems module

Tasks’ problems

class inginious.common.tasks_problems.CodeProblem(task, problemid, content, translations=None)[source]

Bases: inginious.common.tasks_problems.Problem

Code problem

check_answer(_, __)[source]

Check the answer. Returns four values: the first is either True, False or None, indicating respectively that the answer is valid, invalid, or need to be sent to VM the second is the error message assigned to the task, if any (unused for now) the third is the error message assigned to this problem, if any the fourth is the number of errors in MCQ; should be zero when not a MCQ.

classmethod get_text_fields()[source]

Returns a dict whose keys are the keys of content dict and val is True if value of content[key] is human-readable text

classmethod get_type()[source]

Returns the type of the problem

input_is_consistent(task_input, default_allowed_extension, default_max_size)[source]

Check if an input for this problem is consistent. Return true if this is case, false else

input_type()[source]

Indicates if problem input type

classmethod parse_problem(problem_content)[source]
class inginious.common.tasks_problems.CodeSingleLineProblem(task, problemid, content, translations=None)[source]

Bases: inginious.common.tasks_problems.CodeProblem

Code problem with a single line of input

classmethod get_type()[source]

Returns the type of the problem

class inginious.common.tasks_problems.FileProblem(task, problemid, content, translations=None)[source]

Bases: inginious.common.tasks_problems.Problem

File upload Problem

check_answer(_, __)[source]

Check the answer. Returns four values: the first is either True, False or None, indicating respectively that the answer is valid, invalid, or need to be sent to VM the second is the error message assigned to the task, if any (unused for now) the third is the error message assigned to this problem, if any the fourth is the number of errors in MCQ; should be zero when not a MCQ.

classmethod get_text_fields()[source]

Returns a dict whose keys are the keys of content dict and val is True if value of content[key] is human-readable text

classmethod get_type()[source]

Returns the type of the problem

input_is_consistent(task_input, default_allowed_extension, default_max_size)[source]

Check if an input for this problem is consistent. Return true if this is case, false else

input_type()[source]

Indicates if problem input type

classmethod parse_problem(problem_content)[source]
class inginious.common.tasks_problems.MatchProblem(task, problemid, content, translations=None)[source]

Bases: inginious.common.tasks_problems.Problem

Display an input box and check that the content is correct

check_answer(task_input, language)[source]

Check the answer. Returns four values: the first is either True, False or None, indicating respectively that the answer is valid, invalid, or need to be sent to VM the second is the error message assigned to the task, if any (unused for now) the third is the error message assigned to this problem, if any the fourth is the number of errors in MCQ; should be zero when not a MCQ.

classmethod get_text_fields()[source]

Returns a dict whose keys are the keys of content dict and val is True if value of content[key] is human-readable text

classmethod get_type()[source]

Returns the type of the problem

input_is_consistent(task_input, default_allowed_extension, default_max_size)[source]

Check if an input for this problem is consistent. Return true if this is case, false else

input_type()[source]

Indicates if problem input type

classmethod parse_problem(problem_content)[source]
class inginious.common.tasks_problems.MultipleChoiceProblem(task, problemid, content, translations=None)[source]

Bases: inginious.common.tasks_problems.Problem

Multiple choice problems

allow_multiple()[source]

Returns true if this multiple choice problem allows checking multiple answers

check_answer(task_input, language)[source]

Check the answer. Returns four values: the first is either True, False or None, indicating respectively that the answer is valid, invalid, or need to be sent to VM the second is the error message assigned to the task, if any (unused for now) the third is the error message assigned to this problem, if any the fourth is the number of errors in MCQ; should be zero when not a MCQ.

get_choice_with_index(index)[source]

Return the choice with index=index

classmethod get_text_fields()[source]

Returns a dict whose keys are the keys of content dict and val is True if value of content[key] is human-readable text

classmethod get_type()[source]

Returns the type of the problem

input_is_consistent(task_input, default_allowed_extension, default_max_size)[source]

Check if an input for this problem is consistent. Return true if this is case, false else

input_type()[source]

Indicates if problem input type

classmethod parse_problem(problem_content)[source]
class inginious.common.tasks_problems.Problem(task, problemid, content, translations=None)[source]

Bases: object

Basic problem

check_answer(task_input, language)[source]

Check the answer. Returns four values: the first is either True, False or None, indicating respectively that the answer is valid, invalid, or need to be sent to VM the second is the error message assigned to the task, if any (unused for now) the third is the error message assigned to this problem, if any the fourth is the number of errors in MCQ; should be zero when not a MCQ.

get_id()[source]

Get the id of this problem

get_name(language=None)[source]

Get the name of this problem

get_original_content()[source]

Get a dict fully describing this sub-problem

get_task()[source]

Get the task containing this problem

classmethod get_text_fields()[source]

Returns a dict whose keys are the keys of content dict and val is True if value of content[key] is human-readable text

classmethod get_type()[source]

Returns the type of the problem

gettext(language, *args, **kwargs)[source]
input_is_consistent(task_input, default_allowed_extension, default_max_size)[source]

Check if an input for this problem is consistent. Return true if this is case, false else

input_type()[source]

Indicates if problem input type

classmethod parse_problem(problem_content)[source]
inginious.frontend package

Package that implements a webapp for INGInious

Subpackages
inginious.frontend.pages package

Frontend pages (controllers)

Subpackages
inginious.frontend.pages.api package

REST API for the webapp

Submodules
inginious.frontend.pages.api.auth_methods module

Auth methods

class inginious.frontend.pages.api.auth_methods.APIAuthMethods[source]

Bases: inginious.frontend.pages.api._api_page.APIPage

Endpoint /api/v0/auth_methods

API_GET()[source]

Returns all the auth methods available. (200 OK)

Response: list of auth methods. The value of the dict is an auth method, represented by:

id
id of the auth method
name
the name of the authentication method, typically displayed by the webapp
input

a dictionary containing as key the name of the input (in the HTML sense of name), and, as value, a dictionary containing two fields:

name
the placeholder for the input
type
text or password
inginious.frontend.pages.api.authentication module

Authentication

class inginious.frontend.pages.api.authentication.APIAuthentication[source]

Bases: inginious.frontend.pages.api._api_page.APIPage

Endpoint /api/v0/authentication

API_GET()[source]

Returns {“authenticated”: false} or {“authenticated”: true, “username”: “your_username”} (always 200 OK)

API_POST()[source]

Authenticates the remote client. Takes as input:

auth_method_id
an id for an auth method as returned be /api/v0/auth_methods
input_key_1
the first input key and its value
input_key_2
the first input key and its value

Response: a dict in the form {“status”: “success”} (200 OK) or {“status”: “error”} (403 Forbidden)

inginious.frontend.pages.api.courses module

Courses

class inginious.frontend.pages.api.courses.APICourses[source]

Bases: inginious.frontend.pages.api._api_page.APIAuthenticatedPage

Endpoint /api/v0/courses(/[a-zA-Z_-.0-9]+)?

API_GET(courseid=None)[source]

List courses available to the connected client. Returns a dict in the form

{
    "courseid1":
    {
        "name": "Name of the course",     #the name of the course
        "require_password": False,        #indicates if this course requires a password or not
        "is_registered": False,           #indicates if the user is registered to this course or not
        "tasks":                          #only appears if is_registered is True
        {
            "taskid1": "name of task1",
            "taskid2": "name of task2"
            #...
        },
        "grade": 0.0                      #the current grade in the course. Only appears if is_registered is True
    }
    #...
}

If you use the endpoint /api/v0/courses/the_course_id, this dict will contain one entry or the page will return 404 Not Found.

inginious.frontend.pages.api.submissions module

Submissions

class inginious.frontend.pages.api.submissions.APISubmissionSingle[source]

Bases: inginious.frontend.pages.api._api_page.APIAuthenticatedPage

Endpoint /api/v0/courses/[a-zA-Z_-.0-9]+/tasks/[a-zA-Z_-.0-9]+/submissions/[a-zA-Z_-.0-9]+

API_GET(courseid, taskid, submissionid)[source]

List all the submissions that the connected user made. Returns list of the form

[
    {
        "id": "submission_id1",
        "submitted_on": "date",
        "status" : "done",          #can be "done", "waiting", "error" (execution status of the task).
        "grade": 0.0,
        "input": {},                #the input data. File are base64 encoded.
        "result" : "success"        #only if status=done. Result of the execution.
        "feedback": ""              #only if status=done. the HTML global feedback for the task
        "problems_feedback":        #only if status=done. HTML feedback per problem. Some pid may be absent.
        {
            "pid1": "feedback1",
            #...
        }
    }
    #...
]

If you use the endpoint /api/v0/courses/the_course_id/tasks/the_task_id/submissions/submissionid, this dict will contain one entry or the page will return 404 Not Found.

class inginious.frontend.pages.api.submissions.APISubmissions[source]

Bases: inginious.frontend.pages.api._api_page.APIAuthenticatedPage

Endpoint /api/v0/courses/[a-zA-Z_-.0-9]+/tasks/[a-zA-Z_-.0-9]+/submissions

API_GET(courseid, taskid)[source]

List all the submissions that the connected user made. Returns dicts in the form

[
    {
        "id": "submission_id1",
        "submitted_on": "date",
        "status" : "done",          #can be "done", "waiting", "error" (execution status of the task).
        "grade": 0.0,
        "input": {},                #the input data. File are base64 encoded.
        "result" : "success"        #only if status=done. Result of the execution.
        "feedback": ""              #only if status=done. the HTML global feedback for the task
        "problems_feedback":        #only if status=done. HTML feedback per problem. Some pid may be absent.
        {
            "pid1": "feedback1",
            #...
        }
    }
    #...
]

If you use the endpoint /api/v0/courses/the_course_id/tasks/the_task_id/submissions/submissionid, this dict will contain one entry or the page will return 404 Not Found.

API_POST(courseid, taskid)[source]

Creates a new submissions. Takes as (POST) input the key of the subproblems, with the value assigned each time.

Returns

  • an error 400 Bad Request if all the input is not (correctly) given,
  • an error 403 Forbidden if you are not allowed to create a new submission for this task
  • an error 404 Not found if the course/task id not found
  • an error 500 Internal server error if the grader is not available,
  • 200 Ok, with {“submissionid”: “the submission id”} as output.
inginious.frontend.pages.api.tasks module

Tasks

class inginious.frontend.pages.api.tasks.APITasks[source]

Bases: inginious.frontend.pages.api._api_page.APIAuthenticatedPage

Endpoint /api/v0/courses/[a-zA-Z_-.0-9]+/tasks(/[a-zA-Z_-.0-9]+)?

API_GET(courseid, taskid=None)[source]

List tasks available to the connected client. Returns a dict in the form

{
    "taskid1":
    {
        "name": "Name of the course",     #the name of the course
        "authors": [],
        "deadline": "",
        "status": "success"               # can be "succeeded", "failed" or "notattempted"
        "grade": 0.0,
        "grade_weight": 0.0,
        "context": ""                     # context of the task, in RST
        "problems":                       # dict of the subproblems
        {
                                          # see the format of task.yaml for the content of the dict. Contains everything but
                                          # responses of multiple-choice and match problems.
        }
    }
    #...
}

If you use the endpoint /api/v0/courses/the_course_id/tasks/the_task_id, this dict will contain one entry or the page will return 404 Not Found.

inginious.frontend.pages.course_admin package

Course administration

Submodules
inginious.frontend.pages.course_admin.aggregation_edit module

Pages that allow editing of tasks

class inginious.frontend.pages.course_admin.aggregation_edit.CourseEditAggregation[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousAdminPage

Edit a task

GET_AUTH(courseid, aggregationid='')[source]

Edit a aggregation

POST_AUTH(courseid, aggregationid='')[source]

Edit a aggregation

display_page(course, aggregationid='', msg='', error=False)[source]
get_user_lists(course, aggregationid='')[source]

Get the available student and tutor lists for aggregation edition

update_aggregation(course, aggregationid, new_data)[source]

Update aggregation and returns a list of errored students

inginious.frontend.pages.course_admin.aggregation_info module
class inginious.frontend.pages.course_admin.aggregation_info.CourseAggregationInfoPage[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousAdminPage

List information about a aggregation

GET_AUTH(courseid, aggregationid)[source]

GET request

page(course, aggregationid)[source]

Get all data and display the page

submission_url_generator(aggregationid, taskid)[source]

Generates a submission url

inginious.frontend.pages.course_admin.aggregation_list module
class inginious.frontend.pages.course_admin.aggregation_list.CourseAggregationListPage[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousAdminPage

Course administration page: list of aggregations

GET_AUTH(courseid)[source]

GET request

POST_AUTH(courseid)[source]

POST request

page(course, msg='', error=False)[source]

Get all data and display the page

submission_url_generator(aggregationid)[source]

Generates a submission url

inginious.frontend.pages.course_admin.aggregation_task module
inginious.frontend.pages.course_admin.classroom_edit module

Pages that allow editing of tasks

class inginious.frontend.pages.course_admin.classroom_edit.CourseEditClassroom[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousAdminPage

Edit a task

GET_AUTH(courseid, classroomid)[source]

Edit a classroom

POST_AUTH(courseid, classroomid)[source]

Edit a classroom

get_user_lists(course, classroomid)[source]

Get the available student and tutor lists for classroom edition

update_classroom(course, classroomid, new_data)[source]

Update classroom and returns a list of errored students

inginious.frontend.pages.course_admin.danger_zone module
class inginious.frontend.pages.course_admin.danger_zone.CourseDangerZonePage[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousAdminPage

Course administration page: list of classrooms

GET_AUTH(courseid)[source]

GET request

POST_AUTH(courseid)[source]

POST request

delete_course(courseid)[source]

Erase all course data

dump_course(courseid)[source]

Create a zip file containing all information about a given course in database and then remove it from db

get_backup_list(course)[source]
page(course, msg='', error=False)[source]

Get all data and display the page

restore_course(courseid, backup)[source]

Restores a course of given courseid to a date specified in backup (format : YYYYMMDD.HHMMSS)

wipe_course(courseid)[source]
inginious.frontend.pages.course_admin.download module
class inginious.frontend.pages.course_admin.download.CourseDownloadSubmissions[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousSubmissionAdminPage

Batch operation management

GET_AUTH(courseid)[source]

GET request

POST_AUTH(courseid)[source]

GET request

valid_formats()[source]
inginious.frontend.pages.course_admin.settings module
class inginious.frontend.pages.course_admin.settings.CourseSettings[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousAdminPage

Couse settings

GET_AUTH(courseid)[source]

GET request

POST_AUTH(courseid)[source]

POST request

page(course, errors=None, saved=False)[source]

Get all data and display the page

inginious.frontend.pages.course_admin.student_info module
class inginious.frontend.pages.course_admin.student_info.CourseStudentInfoPage[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousAdminPage

List information about a student

GET_AUTH(courseid, username)[source]

GET request

page(course, username)[source]

Get all data and display the page

submission_url_generator(username, taskid)[source]

Generates a submission url

inginious.frontend.pages.course_admin.student_list module
class inginious.frontend.pages.course_admin.student_list.CourseStudentListPage[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousAdminPage

Course administration page: list of registered students

GET_AUTH(courseid)[source]

GET request

POST_AUTH(courseid)[source]

POST request

page(course, error='', post=False)[source]

Get all data and display the page

submission_url_generator(username)[source]

Generates a submission url

inginious.frontend.pages.course_admin.student_task module
inginious.frontend.pages.course_admin.submission module
class inginious.frontend.pages.course_admin.submission.SubmissionPage[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousAdminPage

List information about a task done by a student

GET_AUTH(submissionid)[source]

GET request

POST_AUTH(submissionid)[source]
fetch_submission(submissionid)[source]
page(course, task, submission)[source]

Get all data and display the page

inginious.frontend.pages.course_admin.task_edit module

Pages that allow editing of tasks

class inginious.frontend.pages.course_admin.task_edit.CourseEditTask[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousAdminPage

Edit a task

GET_AUTH(courseid, taskid)[source]

Edit a task

POST_AUTH(courseid, taskid)[source]

Edit a task

classmethod contains_is_html(data)[source]

Detect if the problem has at least one “xyzIsHTML” key

classmethod dict_from_prefix(prefix, dictionary)[source]
>>> from collections import OrderedDict
>>> od = OrderedDict()
>>> od["problem[q0][a]"]=1
>>> od["problem[q0][b][c]"]=2
>>> od["problem[q1][first]"]=1
>>> od["problem[q1][second]"]=2
>>> AdminCourseEditTask.dict_from_prefix("problem",od)
OrderedDict([('q0', OrderedDict([('a', 1), ('b', OrderedDict([('c', 2)]))])), ('q1', OrderedDict([('first', 1), ('second', 2)]))])
parse_problem(problem_content)[source]

Parses a problem, modifying some data

wipe_task(courseid, taskid)[source]

Wipe the data associated to the taskid from DB

inginious.frontend.pages.course_admin.task_edit_file module

Allow to create/edit/delete/move/download files associated to tasks

class inginious.frontend.pages.course_admin.task_edit_file.CourseTaskFiles[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousAdminPage

Edit a task

GET_AUTH(courseid, taskid)[source]

Edit a task

POST_AUTH(courseid, taskid)[source]

Upload or modify a file

action_create(courseid, taskid, path)[source]

Delete a file or a directory

action_delete(courseid, taskid, path)[source]

Delete a file or a directory

action_download(courseid, taskid, path)[source]

Download a file or a directory

action_edit(courseid, taskid, path)[source]

Edit a file

action_edit_save(courseid, taskid, path, content)[source]

Save an edited file

action_rename(courseid, taskid, path, new_path)[source]

Delete a file or a directory

action_upload(courseid, taskid, path, fileobj)[source]

Upload a file

classmethod get_task_filelist(task_factory, courseid, taskid)[source]

Returns a flattened version of all the files inside the task directory, excluding the files task.* and hidden files. It returns a list of tuples, of the type (Integer Level, Boolean IsDirectory, String Name, String CompleteName)

show_tab_file(courseid, taskid, error=None)[source]

Return the file tab

verify_path(courseid, taskid, path, new_path=False)[source]

Return the real wanted path (relative to the INGInious root) or None if the path is not valid/allowed

inginious.frontend.pages.course_admin.task_info module
class inginious.frontend.pages.course_admin.task_info.CourseTaskInfoPage[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousAdminPage

List informations about a task

GET_AUTH(courseid, taskid)[source]

GET request

aggregation_submission_url_generator(task, aggregation)[source]

Generates a submission url

individual_submission_url_generator(task, task_data)[source]

Generates a submission url

page(course, task)[source]

Get all data and display the page

inginious.frontend.pages.course_admin.task_list module
class inginious.frontend.pages.course_admin.task_list.CourseTaskListPage[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousAdminPage

List informations about all tasks

GET_AUTH(courseid)[source]

GET request

POST_AUTH(courseid)[source]

POST request

page(course)[source]

Get all data and display the page

submission_url_generator(taskid)[source]

Generates a submission url

inginious.frontend.pages.course_admin.utils module

Utilities for administration pages

class inginious.frontend.pages.course_admin.utils.CourseRedirect[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousAdminPage

Redirect admins to /settings and tutors to /task

GET_AUTH(courseid)[source]

GET request

POST_AUTH(courseid)[source]

POST request

class inginious.frontend.pages.course_admin.utils.INGIniousAdminPage[source]

Bases: inginious.frontend.pages.utils.INGIniousAuthPage

An improved version of INGIniousAuthPage that checks rights for the administration

get_course_and_check_rights(courseid, taskid=None, allow_all_staff=True)[source]

Returns the course with id courseid and the task with id taskid, and verify the rights of the user. Raise web.notfound() when there is no such course of if the users has not enough rights.

Parameters:
  • courseid – the course on which to check rights
  • taskid – If not None, returns also the task with id taskid
  • allow_all_staff – allow admins AND tutors to see the page. If false, all only admins.

:returns (Course, Task)

class inginious.frontend.pages.course_admin.utils.INGIniousSubmissionAdminPage[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousAdminPage

An INGIniousAdminPage containing some common methods between download/replay pages

get_selected_submissions(course, filter_type, selected_tasks, users, aggregations, stype)[source]

Returns the submissions that have been selected by the admin :param course: course :param filter_type: users or aggregations :param selected_tasks: selected tasks id :param users: selected usernames :param aggregations: selected aggregations :param stype: single or all submissions :return:

show_page_params(course, user_input)[source]
class inginious.frontend.pages.course_admin.utils.UnicodeWriter(f, dialect=<class 'csv.excel'>, encoding='utf-8', **kwds)[source]

Bases: object

A CSV writer which will write rows to CSV file “f”, which is encoded in the given encoding.

writerow(row)[source]

Writes a row to the CSV file

writerows(rows)[source]

Writes multiple rows to the CSV file

inginious.frontend.pages.course_admin.utils.get_menu(course, current, renderer, plugin_manager, user_manager)[source]

Returns the HTML of the menu used in the administration. `current` is the current page of section

inginious.frontend.pages.course_admin.utils.make_csv(data)[source]

Returns the content of a CSV file with the data of the dict/list data

Submodules
inginious.frontend.pages.aggregation module

Index page

class inginious.frontend.pages.aggregation.AggregationPage[source]

Bases: inginious.frontend.pages.utils.INGIniousAuthPage

Aggregation page

GET_AUTH(courseid)[source]

GET request

inginious.frontend.pages.course module

Course page

class inginious.frontend.pages.course.CoursePage[source]

Bases: inginious.frontend.pages.utils.INGIniousPage

Course page

GET(courseid)[source]

GET request

POST(courseid)[source]

POST request

get_course(courseid)[source]

Return the course

show_page(course)[source]

Prepares and shows the course page

inginious.frontend.pages.index module

Index page

class inginious.frontend.pages.index.IndexPage[source]

Bases: inginious.frontend.pages.utils.INGIniousStaticPage

Index page

GET()[source]

Display main course list page

POST()[source]

Display main course list page

inginious.frontend.pages.maintenance module

Maintenance page

class inginious.frontend.pages.maintenance.MaintenancePage[source]

Bases: inginious.frontend.pages.utils.INGIniousPage

Maintenance page

GET()[source]

GET request

POST()[source]

POST request

inginious.frontend.pages.tasks module

Task page

class inginious.frontend.pages.tasks.BaseTaskPage(calling_page)[source]

Bases: object

Display a task (and allow to reload old submission/file uploaded during a submission)

GET(courseid, taskid, isLTI)[source]

GET request

POST(courseid, taskid, isLTI)[source]

POST a new submission

set_selected_submission(course, task, submissionid)[source]

Set submission whose id is submissionid to selected grading submission for the given course/task. Returns a boolean indicating whether the operation was successful or not.

submission_to_json(task, data, debug, reloading=False, replace=False, tags={})[source]

Converts a submission to json (keeps only needed fields)

class inginious.frontend.pages.tasks.TaskPage[source]

Bases: inginious.frontend.pages.utils.INGIniousPage

GET(courseid, taskid)[source]
POST(courseid, taskid)[source]
class inginious.frontend.pages.tasks.TaskPageStaticDownload[source]

Bases: inginious.frontend.pages.utils.INGIniousPage

Allow to download files stored in the task folder

GET(courseid, taskid, path)[source]

GET request

is_lti_page()[source]

True if the current page allows LTI sessions. False else.

inginious.frontend.pages.utils module

Some utils for all the pages

class inginious.frontend.pages.utils.INGIniousAuthPage[source]

Bases: inginious.frontend.pages.utils.INGIniousPage

Augmented version of INGIniousPage that checks if user is authenticated.

GET(*args, **kwargs)[source]

Checks if user is authenticated and calls GET_AUTH or performs logout. Otherwise, returns the login template.

GET_AUTH(*args, **kwargs)[source]
POST(*args, **kwargs)[source]

Checks if user is authenticated and calls POST_AUTH or performs login and calls GET_AUTH. Otherwise, returns the login template.

POST_AUTH(*args, **kwargs)[source]
class inginious.frontend.pages.utils.INGIniousPage[source]

Bases: object

A base for all the pages of the INGInious webapp. Contains references to the PluginManager, the CourseFactory, and the SubmissionManager

app

Returns the web application singleton

backup_dir

Backup directory

containers

Available containers

course_factory

Returns the course factory singleton

database

Returns the database singleton

default_allowed_file_extensions

List of allowed file extensions

default_max_file_size

Default maximum file size for upload

gridfs

Returns the GridFS singleton

is_lti_page

True if the current page allows LTI sessions. False else.

logger

Logger

lti_outcome_manager

Returns the LTIOutcomeManager singleton

plugin_manager

Returns the plugin manager singleton

submission_manager

Returns the submission manager singleton

task_factory

Returns the task factory singleton

template_helper

Returns the Template Helper singleton

user_manager

Returns the user manager singleton

Returns the link to the web terminal

class inginious.frontend.pages.utils.INGIniousStaticPage[source]

Bases: inginious.frontend.pages.utils.INGIniousPage

GET(page)[source]
POST(page)[source]
cache = {}
show_page(page)[source]
class inginious.frontend.pages.utils.LogOutPage[source]

Bases: inginious.frontend.pages.utils.INGIniousAuthPage

GET_AUTH(*args, **kwargs)[source]
POST_AUTH(*args, **kwargs)[source]
class inginious.frontend.pages.utils.SignInPage[source]

Bases: inginious.frontend.pages.utils.INGIniousAuthPage

GET()[source]

Checks if user is authenticated and calls GET_AUTH or performs logout. Otherwise, returns the login template.

GET_AUTH(*args, **kwargs)[source]
POST_AUTH(*args, **kwargs)[source]
inginious.frontend.plugins package

Plugins for the webapp of INGInious

Subpackages
inginious.frontend.plugins.auth package

Auth plugins

Submodules
inginious.frontend.plugins.auth.db_auth module
inginious.frontend.plugins.auth.demo_auth module
inginious.frontend.plugins.auth.ldap_auth module
inginious.frontend.plugins.contests package

An algorithm contest plugin for INGInious. Based on the same principles than contests like ACM-ICPC.

class inginious.frontend.plugins.contests.ContestAdmin[source]

Bases: inginious.frontend.pages.course_admin.utils.INGIniousAdminPage

Contest settings for a course

GET_AUTH(courseid)[source]

GET request: simply display the form

POST_AUTH(courseid)[source]

POST request: update the settings

save_contest_data(course, contest_data)[source]

Saves updated contest data for the course

class inginious.frontend.plugins.contests.ContestScoreboard[source]

Bases: inginious.frontend.pages.utils.INGIniousAuthPage

Displays the scoreboard of the contest

GET_AUTH(courseid)[source]
inginious.frontend.plugins.contests.add_admin_menu(course)[source]

Add a menu for the contest settings in the administration

inginious.frontend.plugins.contests.additional_headers()[source]

Additional HTML headers

inginious.frontend.plugins.contests.course_menu(course, template_helper)[source]

Displays some informations about the contest on the course page

inginious.frontend.plugins.contests.get_contest_data(course)[source]

Returns the settings of the contest for this course

inginious.frontend.plugins.contests.init(plugin_manager, course_factory, client, config)[source]

Init the contest plugin. Available configuration:

{
    "plugin_module": "inginious.frontend.plugins.contests"
}
inginious.frontend.plugins.contests.task_accessibility(course, task, default)[source]
Submodules
inginious.frontend.plugins.contests.contests module
inginious.frontend.plugins.scoreboard package

A scoreboard, based on the usage of the “custom” dict in submissions. It uses the key “score” to retrieve score from submissions

class inginious.frontend.plugins.scoreboard.ScoreBoard[source]

Bases: inginious.frontend.pages.utils.INGIniousAuthPage

Page displaying a specific scoreboard

GET_AUTH(courseid, scoreboardid)[source]

GET request

class inginious.frontend.plugins.scoreboard.ScoreBoardCourse[source]

Bases: inginious.frontend.pages.utils.INGIniousAuthPage

Page displaying the different available scoreboards for the course

GET_AUTH(courseid)[source]

GET request

inginious.frontend.plugins.scoreboard.course_menu(course, template_helper)[source]

Displays the link to the scoreboards on the course page, if the plugin is activated for this course

inginious.frontend.plugins.scoreboard.init(plugin_manager, _, _2, _3)[source]

Init the plugin. Available configuration in configuration.yaml:

- plugin_module: "inginious.frontend.plugins.scoreboard"

Available configuration in course.yaml:

- scoreboard: #you can define multiple scoreboards
    - content: "taskid1" #creates a scoreboard for taskid1
      name: "Scoreboard task 1"
    - content: ["taskid2", "taskid3"] #creates a scoreboard for taskid2 and taskid3 (sum of both score is taken as overall score)
      name: "Scoreboard for task 2 and 3"
    - content: {"taskid4": 2, "taskid5": 3} #creates a scoreboard where overall score is 2*score of taskid4 + 3*score of taskid5
      name: "Another scoreboard"
      reverse: True #reverse the score (less is better)
inginious.frontend.plugins.scoreboard.sort_func(overall_result_per_user, reverse)[source]
inginious.frontend.plugins.scoreboard.task_menu(course, task, template_helper)[source]

Displays the link to the scoreboards on the task page, if the plugin is activated for this course and the task is used in scoreboards

inginious.frontend.plugins.task_file_readers package

Additional task files managers for INGInious

Submodules
inginious.frontend.plugins.task_file_readers.json_reader module

JSON task file manager.

class inginious.frontend.plugins.task_file_readers.json_reader.TaskJSONFileReader[source]

Bases: inginious.common.task_file_readers.abstract_reader.AbstractTaskFileReader

Read and write task descriptions in JSON

dump(data)[source]

Dump descriptor and returns the content that should be written to the task file

classmethod get_ext()[source]

Returns the task file extension. Must be @classmethod!

load(content)[source]

Parses file_content and returns a dict describing a task

inginious.frontend.plugins.task_file_readers.json_reader.init(plugin_manager, _, _2, _3)[source]

Init the plugin. Configuration:

plugins:
    - plugin_module: inginious.frontend.plugins.task_file_readers.json_reader
inginious.frontend.plugins.task_file_readers.rst_reader module
Submodules
inginious.frontend.plugins.client_test module
inginious.frontend.plugins.demo_page module
inginious.frontend.plugins.edx module
inginious.frontend.plugins.git_repo module

A plugin that allows to save submissions to a Git repository

class inginious.frontend.plugins.git_repo.SubmissionGitSaver(plugin_manager, config)[source]

Bases: threading.Thread

Thread class that saves results from submission in the git repo. It must be a thread as a git commit can take some time and because we extract archives returned by the Client. But it must also be launched only one time as our git operations are not really process/tread-safe ;-)

add(submission, archive, _)[source]

Add a new submission to the repo (add the to queue, will be saved async)

run()[source]

Method representing the thread’s activity.

You may override this method in a subclass. The standard run() method invokes the callable object passed to the object’s constructor as the target argument, if any, with sequential and keyword arguments taken from the args and kwargs arguments, respectively.

save(submission, result, grade, problems, tests, custom, archive)[source]

saves a new submission in the repo (done async)

inginious.frontend.plugins.git_repo.init(plugin_manager, _, _2, config)[source]

Init the plugin

Available configuration:

plugins:
    - plugin_module: inginious.frontend.plugins.git_repo
      repo_directory: "./repo_submissions"
inginious.frontend.plugins.simple_grader module

Allow the webapp to act as a simple POST grader

inginious.frontend.plugins.simple_grader.init(plugin_manager, course_factory, client, config)[source]

Init the external grader plugin. This simple grader allows only anonymous requests, and submissions are not stored in database.

Available configuration:

plugins:
    - plugin_module: inginious.frontend.plugins.simple_grader
      courseid : "external"
      page_pattern: "/external"
      return_fields: "^(result|text|problems)$"

The grader will only return fields that are in the job return dict if their key match return_fields.

Different types of request are available : see documentation

inginious.frontend.tests package
Submodules
inginious.frontend.tests.SeleniumTest module
class inginious.frontend.tests.SeleniumTest.SeleniumTest(methodName='runTest')[source]

Bases: unittest.case.TestCase

close_alert_and_get_its_text()[source]
is_alert_present()[source]
is_element_present(how, what)[source]
setUp()[source]

Hook method for setting up the test fixture before exercising it.

tearDown()[source]

Hook method for deconstructing the test fixture after testing it.

wait_for_presence_css(selector)[source]
inginious.frontend.tests.TestLogin module
class inginious.frontend.tests.TestLogin.LoggedInTest(methodName='runTest')[source]

Bases: inginious.frontend.tests.SeleniumTest.SeleniumTest

login = 'test'
password = 'test'
setUp()[source]

Hook method for setting up the test fixture before exercising it.

class inginious.frontend.tests.TestLogin.RegisteredTest(methodName='runTest')[source]

Bases: inginious.frontend.tests.TestLogin.LoggedInTest

course = 'test'
setUp()[source]

Hook method for setting up the test fixture before exercising it.

class inginious.frontend.tests.TestLogin.TestLogin(methodName='runTest')[source]

Bases: inginious.frontend.tests.SeleniumTest.SeleniumTest

test_login()[source]
class inginious.frontend.tests.TestLogin.TestRegistration(methodName='runTest')[source]

Bases: inginious.frontend.tests.TestLogin.LoggedInTest

login = 'test3'
password = 'test'
test_register()[source]
test_unregister()[source]
inginious.frontend.tests.TestParsableText module
class inginious.frontend.tests.TestParsableText.TestHookManager[source]

Bases: object

test_code()[source]
test_failing_parser_injection()[source]
test_hidden_until_after()[source]
test_hidden_until_before()[source]
test_hidden_until_before_admin()[source]
test_html_tidy()[source]
test_parsable_text_once()[source]
test_str()[source]
test_unicode()[source]
test_wrong_rst_injection()[source]
inginious.frontend.tests.TestTaskDisplay module
class inginious.frontend.tests.TestTaskDisplay.TestDisplayAdmin(methodName='runTest')[source]

Bases: inginious.frontend.tests.TestTaskDisplay.TestDisplaySuperAdmin

login = 'test2'
password = 'test'
class inginious.frontend.tests.TestTaskDisplay.TestDisplaySuperAdmin(methodName='runTest')[source]

Bases: inginious.frontend.tests.TestLogin.LoggedInTest

login = 'test'
password = 'test'
test_register()[source]
class inginious.frontend.tests.TestTaskDisplay.TestDisplayUserAfterDeadline(methodName='runTest')[source]

Bases: inginious.frontend.tests.TestLogin.RegisteredTest

course = 'test'
login = 'test3'
password = 'test'
test_register()[source]
inginious.frontend.tests.TestTaskSubmission module
class inginious.frontend.tests.TestTaskSubmission.TestTaskSubmission(methodName='runTest')[source]

Bases: inginious.frontend.tests.TestLogin.LoggedInTest

login = 'test'
password = 'test'
test_submit()[source]
Submodules
inginious.frontend.accessible_time module

Contains AccessibleTime, class that represents the period of time when a course/task is accessible

class inginious.frontend.accessible_time.AccessibleTime(val=None)[source]

Bases: object

represents the period of time when a course/task is accessible

after_start(when=None)[source]

Returns True if the task/course is or have been accessible in the past

before_start(when=None)[source]

Returns True if the task/course is not yet accessible

get_end_date()[source]

Return a datetime object, representing the deadline for accessibility

get_start_date()[source]

Return a datetime object, representing the date when the task/course become accessible

get_std_end_date()[source]

If the date is custom, return the end datetime with the format %Y-%m-%d %H:%M:%S. Else, returns “”.

get_std_start_date()[source]

If the date is custom, return the start datetime with the format %Y-%m-%d %H:%M:%S. Else, returns “”.

is_always_accessible()[source]

Returns true if the course/task is always accessible

is_never_accessible()[source]

Returns true if the course/task is never accessible

is_open(when=None)[source]

Returns True if the course/task is still open

inginious.frontend.accessible_time.parse_date(date, default=None)[source]

Parse a valid date

inginious.frontend.arch_helper module
inginious.frontend.arch_helper.create_arch(configuration, tasks_fs, context)[source]
Helper that can start a simple complete INGInious arch locally if needed, or a client to a remote backend.
Intended to be used on command line, makes uses of exit() and the logger inginious.frontend.
Parameters:
  • configuration – configuration dict
  • tasks_fs – FileSystemProvider to the courses/tasks folders
  • context – a ZMQ context
  • is_testing – boolean
Returns:

a Client object

inginious.frontend.arch_helper.start_asyncio_and_zmq(debug_asyncio=False)[source]

Init asyncio and ZMQ. Starts a daemon thread in which the asyncio loops run. :return: a ZMQ context and a Thread object (as a tuple)

inginious.frontend.app module

Starts the webapp

inginious.frontend.app.get_app(config)[source]
Parameters:config – the configuration dict
Returns:A new app
inginious.frontend.courses module

A course class with some modification for users

class inginious.frontend.courses.WebAppCourse(courseid, content, course_fs, task_factory, hook_manager)[source]

Bases: inginious.common.courses.Course

A course with some modification for users

allow_preview()[source]
allow_unregister(plugin_override=True)[source]

Returns True if students can unregister from course

can_students_choose_group()[source]

Returns True if the students can choose their groups

get_access_control_list()[source]

Returns the list of all users allowed by the AC list

get_access_control_method()[source]

Returns either None, “username”, “binding”, or “email”, depending on the method used to verify that users can register to the course

get_accessibility(plugin_override=True)[source]

Return the AccessibleTime object associated with the accessibility of this course

get_admins()[source]

Returns a list containing the usernames of the administrators of this course

get_all_tags()[source]

Return a tuple of lists ([common_tags], [anti_tags], [organisational_tags]) all tags of all tasks of this course Since this is an heavy procedure, we use a cache to cache results. Cache should be updated when a task is modified.

get_all_tags_names_as_list(admin=False, language='en')[source]

Computes and cache two list containing all tags name sorted by natural order on name

get_name(language)[source]

Return the name of this course

get_organisational_tags_to_task()[source]

This build a dict for fast retrive tasks id based on organisational tags. The form of the dict is:

{ ‘org_tag_1’: [‘task_id’, ‘task_id’, …],
‘org_tag_2’ : [‘task_id’, ‘task_id’, …], … }
get_registration_accessibility()[source]

Return the AccessibleTime object associated with the registration

get_registration_password()[source]

Returns the password needed for registration (None if there is no password)

get_staff()[source]

Returns a list containing the usernames of all the staff users

get_tasks()[source]

Get all tasks in this course

get_tutors()[source]

Returns a list containing the usernames of the tutors assigned to this course

is_lti()[source]

True if the current course is in LTI mode

is_open_to_non_staff()[source]

Returns true if the course is accessible by users that are not administrator of this course

is_password_needed_for_registration()[source]

Returns true if a password is needed for registration

is_registration_possible(user_info)[source]

Returns true if users can register for this course

is_user_accepted_by_access_control(user_info)[source]

Returns True if the user is allowed by the ACL

lti_keys()[source]

{name: key} for the LTI customers

lti_send_back_grade()[source]

True if the current course should send back grade to the LTI Tool Consumer

update_all_tags_cache()[source]

Force the cache refreshing

use_classrooms()[source]

Returns True if classrooms are used

inginious.frontend.database_updater module
inginious.frontend.installer module

Custom installer for the web app

class inginious.frontend.installer.Installer(config_path=None)[source]

Bases: object

Custom installer for the WebApp frontend

ask_backend()[source]

Ask the user to choose the backend

configuration_filename()[source]

Returns the name of the configuration file

configure_authentication(database)[source]

Configure the authentication

configure_backup_directory()[source]

Configure backup directory

configure_containers(current_options)[source]

Configures the container dict

configure_misc()[source]

Configure various things

configure_mongodb()[source]

Configure MongoDB

configure_task_directory()[source]

Configure task directory

download_containers(to_download, current_options)[source]

Download the chosen containers on all the agents

ldap_plugin()[source]

Configures the LDAP plugin

run()[source]

Run the installator

support_remote_debugging()[source]

Returns True if the frontend supports remote debugging, False else

test_local_docker_conf()[source]

Test to connect to a local Docker daemon

try_mongodb_opts(host='localhost', database_name='INGInious')[source]

Try MongoDB configuration

inginious.frontend.parsable_text module

Tools to parse text

class inginious.frontend.parsable_text.HiddenUntilDirective(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine)[source]

Bases: docutils.parsers.rst.Directive, object

has_content = True
option_spec = {}
optional_arguments = 0
required_arguments = 1
run()[source]
class inginious.frontend.parsable_text.ParsableText(content, mode='rst', show_everything=False, translation=<gettext.NullTranslations object>)[source]

Bases: object

Allow to parse a string with different parsers

classmethod html(string, show_everything=False, translation=<gettext.NullTranslations object>)[source]

Parses HTML

original_content()[source]

Returns the original content

parse()[source]

Returns parsed text

classmethod rst(string, show_everything=False, translation=<gettext.NullTranslations object>, initial_header_level=3)[source]

Parses reStructuredText

inginious.frontend.plugin_manager module

Plugin Manager

class inginious.frontend.plugin_manager.PluginManager[source]

Bases: inginious.common.hook_manager.HookManager

Registers an manage plugins. The init method inits only the Hook Manager; you have to call the method load() to start the plugins

add_page(pattern, classname)[source]

Add a new page to the web application. Only available after that the Plugin Manager is loaded

add_task_file_manager(task_file_manager)[source]

Add a task file manager. Only available after that the Plugin Manager is loaded

get_database()[source]

Returns the frontend database

get_submission_manager()[source]

Returns the submission manager

get_user_manager()[source]

Returns the user manager

load(client, webpy_app, course_factory, task_factory, database, user_manager, submission_manager, config)[source]

Loads the plugin manager. Must be done after the initialisation of the client

register_auth_method(auth_method)[source]

Register a new authentication method

name
the name of the authentication method, typically displayed by the webapp

input_to_display

Only available after that the Plugin Manager is loaded

exception inginious.frontend.plugin_manager.PluginManagerNotLoadedException[source]

Bases: Exception

inginious.frontend.session_mongodb module

Saves sessions in the database

class inginious.frontend.session_mongodb.MongoStore(database, collection_name='sessions')[source]

Bases: web.session.Store

Allow to store web.py sessions in MongoDB

cleanup(timeout)[source]

Removes all sessions older than timeout seconds. Called automatically on every session access.

decode(sessiondict)[source]

decodes the data to get back the session dict

encode(sessiondict)[source]

encodes session dict as a string

inginious.frontend.session_mongodb.needs_encode(obj)[source]
>>> from re import compile
>>> atomics = (True, 1, 1.0, '', None, compile(''), datetime.now(), b'')
>>> any(needs_encode(i) for i in atomics)
False
>>> needs_encode([1, 2, 3])
False
>>> needs_encode([])
False
>>> needs_encode([1, [2, 3]])
False
>>> needs_encode({})
False
>>> needs_encode({'1': {'2': 3}})
False
>>> needs_encode({'1': [2]})
False
>>> needs_encode(b'1')
False

Objects that don’t round trip need encoding:

>>> needs_encode(tuple())

True >>> needs_encode(set()) True >>> needs_encode([1, [set()]]) True >>> needs_encode({‘1’: {‘2’: set()}}) True

Mongo rejects dicts with non-string keys so they need encoding too:

>>> needs_encode({1: 2})

True >>> needs_encode({‘1’: {None: True}}) True

inginious.frontend.static_middleware module

A middleware for Web.py that serves static content

class inginious.frontend.static_middleware.StaticApp(base_path, environ, start_response)[source]

Bases: web.httpserver.StaticApp, object

WSGI application for serving static files.

translate_path(path)[source]

Translate a /-separated PATH to the local filename syntax.

Components that mean special things to the local file system (e.g. drive or directory names) are ignored. (XXX They should probably be diagnosed.)

class inginious.frontend.static_middleware.StaticMiddleware(app, paths)[source]

Bases: object

WSGI middleware for serving static files.

normpath(path)[source]

Normalize the path

inginious.frontend.submission_manager module

Manages submissions

class inginious.frontend.submission_manager.WebAppSubmissionManager(client, user_manager, database, gridfs, hook_manager, lti_outcome_manager)[source]

Bases: object

Manages submissions. Communicates with the database and the client.

add_job(task, inputdata, debug=False)[source]

Add a job in the queue and returns a submission id. :param task: Task instance :type task: inginious.frontend.tasks.WebAppTask :param inputdata: the input as a dictionary :type inputdata: dict :param debug: If debug is true, more debug data will be saved :type debug: bool :returns: the new submission id and the removed submission id

get_available_environments()[source]

:return a list of available environments

get_feedback_from_submission(submission, only_feedback=False, show_everything=False, translation=<gettext.NullTranslations object>)[source]

Get the input of a submission. If only_input is False, returns the full submissions with a dictionnary object at the key “input”. Else, returns only the dictionnary.

If show_everything is True, feedback normally hidden is shown.

get_gridfs()[source]

Returns the GridFS used by the submission manager

get_input_from_submission(submission, only_input=False)[source]

Get the input of a submission. If only_input is False, returns the full submissions with a dictionnary object at the key “input”. Else, returns only the dictionnary.

get_job_queue_info(jobid)[source]
Parameters:jobid – the JOB id (not the submission id!). You should retrieve it before calling this function by calling get_submission(…)[

“job_id”]. :return: If the submission is in the queue, then returns a tuple (nb tasks before running (or -1 if running), approx wait time in seconds)

Else, returns None
get_job_queue_snapshot()[source]
Get a snapshot of the remote backend job queue. May be a cached version.
May not contain recent jobs. May return None if no snapshot is available

Return a tuple of two lists (None, None): jobs_running: a list of tuples in the form

(job_id, is_current_client_job, info, launcher, started_at, max_end) where - job_id is a job id. It may be from another client. - is_current_client_job is a boolean indicating if the client that asked the request has started the job - agent_name is the agent name - info is “courseid/taskid” - launcher is the name of the launcher, which may be anything - started_at the time (in seconds since UNIX epoch) at which the job started - max_end the time at which the job will timeout (in seconds since UNIX epoch), or -1 if no timeout is set
jobs_waiting: a list of tuples in the form
(job_id, is_current_client_job, info, launcher, max_time) where - job_id is a job id. It may be from another client. - is_current_client_job is a boolean indicating if the client that asked the request has started the job - info is “courseid/taskid” - launcher is the name of the launcher, which may be anything - max_time the maximum time that can be used, or -1 if no timeout is set
get_submission(submissionid, user_check=True)[source]

Get a submission from the database

get_submission_archive(submissions, sub_folders, aggregations, archive_file=None)[source]
Parameters:
  • submissions – a list of submissions
  • sub_folders – possible values: []: put all submissions in / [‘taskid’]: put all submissions for each task in a different directory /taskid/ [‘username’]: put all submissions for each user in a different directory /username/ [‘taskid’,’username’]: /taskid/username/ [‘username’,’taskid’]: /username/taskid/
Returns:

a file-like object containing a tgz archive of all the submissions

get_user_last_submissions(limit=5, request=None)[source]

Get last submissions of a user

get_user_submissions(task)[source]

Get all the user’s submissions for a given task

is_done(submissionid_or_submission, user_check=True)[source]

Tells if a submission is done and its result is available

is_running(submissionid, user_check=True)[source]

Tells if a submission is running/in queue

kill_running_submission(submissionid, user_check=True)[source]

Attempt to kill the remote job associated with this submission id. :param submissionid: :param user_check: Check if the current user owns this submission :return: True if the job was killed, False if an error occurred

replay_job(task, submission, copy=False, debug=False)[source]

Replay a submission: add the same job in the queue, keeping submission id, submission date and input data :param submission: Submission to replay :param copy: If copy is true, the submission will be copied to admin submissions before replay :param debug: If debug is true, more debug data will be saved

user_is_submission_owner(submission)[source]

Returns true if the current user is the owner of this jobid, false else

inginious.frontend.submission_manager.update_pending_jobs(database)[source]

Updates pending jobs status in the database

inginious.frontend.task_page_helpers module
inginious.frontend.task_problems module

Displyable problems

class inginious.frontend.task_problems.DisplayableCodeProblem(task, problemid, content, translations=None)[source]

Bases: inginious.common.tasks_problems.CodeProblem, inginious.frontend.task_problems.DisplayableProblem

A basic class to display all BasicCodeProblem derivatives

adapt_input_for_backend(input_data)[source]

Adapt the input from web.py for the inginious.backend

classmethod get_type_name(gettext)[source]
classmethod show_editbox(template_helper, key)[source]

get the edit box html for this problem

classmethod show_editbox_templates(template_helper, key)[source]
show_input(template_helper, language, seed)[source]

Show BasicCodeProblem and derivatives

class inginious.frontend.task_problems.DisplayableCodeSingleLineProblem(task, problemid, content, translations=None)[source]

Bases: inginious.common.tasks_problems.CodeSingleLineProblem, inginious.frontend.task_problems.DisplayableProblem

A displayable single code line problem

adapt_input_for_backend(input_data)[source]

Adapt the input from web.py for the inginious.backend

classmethod get_type_name(gettext)[source]
classmethod show_editbox(template_helper, key)[source]

get the edit box html for this problem

classmethod show_editbox_templates(template_helper, key)[source]
show_input(template_helper, language, seed)[source]

Show InputBox

class inginious.frontend.task_problems.DisplayableFileProblem(task, problemid, content, translations=None)[source]

Bases: inginious.common.tasks_problems.FileProblem, inginious.frontend.task_problems.DisplayableProblem

A displayable code problem

adapt_input_for_backend(input_data)[source]

Adapt the input from web.py for the inginious.backend

classmethod get_type_name(gettext)[source]
classmethod show_editbox(template_helper, key)[source]

get the edit box html for this problem

classmethod show_editbox_templates(template_helper, key)[source]
show_input(template_helper, language, seed)[source]

Show FileBox

class inginious.frontend.task_problems.DisplayableMatchProblem(task, problemid, content, translations=None)[source]

Bases: inginious.common.tasks_problems.MatchProblem, inginious.frontend.task_problems.DisplayableProblem

A displayable match problem

classmethod get_type_name(gettext)[source]
classmethod show_editbox(template_helper, key)[source]

get the edit box html for this problem

classmethod show_editbox_templates(template_helper, key)[source]
show_input(template_helper, language, seed)[source]

Show MatchProblem

class inginious.frontend.task_problems.DisplayableMultipleChoiceProblem(task, problemid, content, translations=None)[source]

Bases: inginious.common.tasks_problems.MultipleChoiceProblem, inginious.frontend.task_problems.DisplayableProblem

A displayable multiple choice problem

classmethod get_type_name(gettext)[source]
classmethod show_editbox(template_helper, key)[source]

get the edit box html for this problem

classmethod show_editbox_templates(template_helper, key)[source]
show_input(template_helper, language, seed)[source]

Show multiple choice problems

class inginious.frontend.task_problems.DisplayableProblem(task, problemid, content, translations=None)[source]

Bases: inginious.common.tasks_problems.Problem

Basic problem

adapt_input_for_backend(input_data)[source]

Adapt the input from web.py for the inginious.backend

classmethod get_renderer(template_helper)[source]

Get the renderer for this class problem

classmethod get_type_name(gettext)[source]
classmethod show_editbox(template_helper, key)[source]

get the edit box html for this problem

classmethod show_editbox_templates(template_helper, key)[source]
show_input(template_helper, language, seed)[source]

get the html for this problem

inginious.frontend.tasks module

Classes modifying basic tasks, problems and boxes classes

class inginious.frontend.tasks.WebAppTask(course, taskid, content, task_fs, hook_manager, task_problem_types)[source]

Bases: inginious.common.tasks.Task

A task that stores additional context information, specific to the web app

adapt_input_for_backend(input_data)[source]

Adapt the input from web.py for the inginious.backend

get_accessible_time(plugin_override=True)[source]

Get the accessible time of this task

get_authors(language)[source]

Return the list of this task’s authors

get_context(language)[source]

Get the context(description) of this task

get_deadline()[source]

Returns a string containing the deadline for this task

get_evaluate()[source]

Indicates the default download for the task

get_grading_weight()[source]

Get the relative weight of this task in the grading

get_name(language)[source]

Returns the name of this task

get_number_input_random()[source]

Return the number of random inputs

get_stored_submissions()[source]

Indicates if only the last submission must be stored for the task

get_submission_limit()[source]

Returns the submission limits et for the task

get_tags()[source]

Get the tuple of list of the task

is_group_task()[source]

Indicates if the task submission mode is per groups

is_visible_by_students()[source]

Returns true if the task is accessible by all students that are not administrator of the course

regenerate_input_random()[source]

Indicates if random inputs should be regenerated

inginious.frontend.tasks_code_boxes module
inginious.frontend.template_helper module

TemplateManager

class inginious.frontend.template_helper.TemplateHelper(plugin_manager, user_manager, default_template_dir, default_layout, default_layout_lti, use_minified=True)[source]

Bases: object

Class accessible from templates that calls function defined in the Python part of the code.

add_css(link)[source]

Add a css file to load

add_javascript(link, position='footer')[source]

Add a javascript file to load. Position can either be “header” or “footer”

add_other(name, func)[source]

Add another callback to the template helper

add_to_template_globals(name, value)[source]

Add a variable to will be accessible in the templates

call(name, **kwargs)[source]
get_custom_renderer(dir_path, layout=True)[source]

Create a template renderer on templates in the directory specified, and returns it. :param dir_path: the path to the template dir. If it is not absolute, it will be taken from the root of the inginious package. :param layout: can either be True (use the base layout of the running app), False (use no layout at all), or the path to the layout to use.

If this path is relative, it is taken from the INGInious package root.
get_renderer(with_layout=True)[source]

Get the default renderer

is_lti()[source]

True if the current session is an LTI one

inginious.frontend.user_manager module

Manages users data and session

exception inginious.frontend.user_manager.AuthInvalidInputException[source]

Bases: Exception

exception inginious.frontend.user_manager.AuthInvalidMethodException[source]

Bases: Exception

class inginious.frontend.user_manager.AuthMethod[source]

Bases: object

allow_share()[source]
Returns:True if the auth method allow sharing, else false
callback(auth_storage)[source]
Parameters:auth_storage – The session auth method storage dict
Returns:User tuple and , or None, if failed
Parameters:auth_storage – The session auth method storage dict
Returns:The authentication link
get_id()[source]
Returns:The auth method id
Returns:The image link
get_name()[source]
Returns:The name of the auth method, to be displayed publicly
share(auth_storage, course, task, submission, language)[source]
Parameters:auth_storage – The session auth method storage dict
Returns:False if error
class inginious.frontend.user_manager.UserManager(session_dict, database, superadmins)[source]

Bases: object

attempt_lti_login()[source]

Given that the current session is an LTI one (session_lti_info does not return None), attempt to find an INGInious user linked to this lti username/consumer_key. If such user exists, logs in using it.

Returns True (resp. False) if the login was successful

auth_user(username, password)[source]

Authenticate the user in database :param username: Username/Login :param password: User password :return: Returns a dict represrnting the user

bind_user(auth_id, user)[source]
connect_user(username, realname, email, language)[source]

Opens a session for the user :param username: Username :param realname: User real name :param email: User email

course_is_open_to_user(course, username=None, lti=None)[source]

Checks if a user is can access a course :param course: a Course object :param username: The username of the user that we want to check. If None, uses self.session_username() :param lti: indicates if the user is currently in a LTI session or not.

  • None to ignore the check
  • True to indicate the user is in a LTI session
  • False to indicate the user is not in a LTI session
  • “auto” to enable the check and take the information from the current session
Returns:True if the user can access the course, False else
course_is_user_registered(course, username=None)[source]

Checks if a user is registered :param course: a Course object :param username: The username of the user that we want to check. If None, uses self.session_username() :return: True if the user is registered, False else

course_register_user(course, username=None, password=None, force=False)[source]

Register a user to the course :param course: a Course object :param username: The username of the user that we want to register. If None, uses self.session_username() :param password: Password for the course. Needed if course.is_password_needed_for_registration() and force != True :param force: Force registration :return: True if the registration succeeded, False else

course_unregister_user(course, username=None)[source]

Unregister a user to the course :param course: a Course object :param username: The username of the user that we want to unregister. If None, uses self.session_username()

create_lti_session(user_id, roles, realname, email, course_id, task_id, consumer_key, outcome_service_url, outcome_result_id, tool_name, tool_desc, tool_url, context_title, context_label)[source]

Creates an LTI cookieless session. Returns the new session id

disconnect_user()[source]

Disconnects the user currently logged-in :param ip_addr: the ip address of the client, that will be logged

get_auth_method(auth_method_id)[source]

:param the auth method id, as provided by get_auth_methods_inputs() :return: AuthMethod if it exists, otherwise None

get_auth_methods()[source]
Returns:The auth methods dict
get_course_aggregations(course)[source]

Returns a list of the course aggregations

get_course_cache(username, course)[source]
Parameters:
  • username – The username
  • course – A Course object
Returns:

a dict containing info about the course, in the form:

{"task_tried": 0, "total_tries": 0, "task_succeeded": 0, "task_grades":{"task_1": 100.0, "task_2": 0.0, ...}}

Note that only the task already seen at least one time will be present in the dict task_grades.

get_course_caches(usernames, course)[source]
Parameters:
  • username – List of username for which we want info. If usernames is None, data from all users will be returned.
  • course – A Course object
Returns:

Returns data of the specified users for a specific course. users is a list of username.

The returned value is a dict:

{"username": {"task_tried": 0, "total_tries": 0, "task_succeeded": 0, "task_grades":{"task_1": 100.0, "task_2": 0.0, ...}}}

Note that only the task already seen at least one time will be present in the dict task_grades.

get_course_registered_users(course, with_admins=True)[source]

Get all the users registered to a course :param course: a Course object :param with_admins: include admins? :return: a list of usernames that are registered to the course

get_course_user_aggregation(course, username=None)[source]

Returns the classroom whose username belongs to :param course: a Course object :param username: The username of the user that we want to register. If None, uses self.session_username() :return: the classroom description

get_task_cache(username, courseid, taskid)[source]

Shorthand for get_task_caches([username], courseid, taskid)[username]

get_task_caches(usernames, courseid, taskid)[source]
Parameters:
  • usernames – List of username for which we want info. If usernames is None, data from all users will be returned.
  • courseid – the course id
  • taskid – the task id
Returns:

A dict in the form:

{
    "username": {
        "courseid": courseid,
        "taskid": taskid,
        "tried": 0,
        "succeeded": False,
        "grade": 0.0
    }
}

get_user_email(username)[source]
Parameters:username
Returns:the email of the user if it can be found, None else
get_user_info(username)[source]
Parameters:username
Returns:a tuple (realname, email) if the user can be found, None else
get_user_realname(username)[source]
Parameters:username
Returns:the real name of the user if it can be found, None else
get_users_info(usernames)[source]
Parameters:usernames – a list of usernames
Returns:a dict, in the form {username: val}, where val is either None if the user cannot be found, or a tuple (realname, email)
has_admin_rights_on_course(course, username=None, include_superadmins=True)[source]

Check if a user can be considered as having admin rights for a course :type course: webapp.custom.courses.WebAppCourse :param username: the username. If None, the username of the currently logged in user is taken :param include_superadmins: Boolean indicating if superadmins should be taken into account :return: True if the user has admin rights, False else

has_staff_rights_on_course(course, username=None, include_superadmins=True)[source]

Check if a user can be considered as having staff rights for a course :type course: webapp.custom.courses.WebAppCourse :param username: the username. If None, the username of the currently logged in user is taken :param include_superadmins: Boolean indicating if superadmins should be taken into account :return: True if the user has staff rights, False else

register_auth_method(auth_method)[source]

Registers an authentication method :param auth_method: an AuthMethod object

session_auth_storage()[source]

Returns the oauth state for login

session_cookieless()[source]

Indicates if the current session is cookieless

session_email()[source]

Returns the email of the current user in the session, if one is open. Else, returns None

session_id()[source]

Returns the current session id

session_language()[source]

Returns the current session language

session_logged_in()[source]

Returns True if a user is currently connected in this session, False else

session_lti_info()[source]

If the current session is an LTI one, returns a dict in the form {

“email”: email, “username”: username “realname”: realname, “roles”: roles, “task”: (course_id, task_id), “outcome_service_url”: outcome_service_url, “outcome_result_id”: outcome_result_id, “consumer_key”: consumer_key

} where all these data where provided by the LTI consumer, and MAY NOT be equivalent to the data contained in database for the currently connected user.

If the current session is not an LTI one, returns None.

session_realname()[source]

Returns the real name of the current user in the session, if one is open. Else, returns None

session_token()[source]

Returns the token of the current user in the session, if one is open. Else, returns None

session_username()[source]

Returns the username from the session, if one is open. Else, returns None

set_session_language(language)[source]
set_session_realname(realname)[source]

Sets the real name of the current user in the session, if one is open.

set_session_token(token)[source]

Sets the token of the current user in the session, if one is open.

task_can_user_submit(task, username=None, only_check=None, lti=None)[source]

returns true if the user can submit his work for this task :param only_check : only checks for ‘groups’, ‘tokens’, or None if all checks :param lti: indicates if the user is currently in a LTI session or not. - None to ignore the check - True to indicate the user is in a LTI session - False to indicate the user is not in a LTI session - “auto” to enable the check and take the information from the current session

task_is_visible_by_user(task, username=None, lti=None)[source]

Returns true if the task is visible by the user :param lti: indicates if the user is currently in a LTI session or not.

  • None to ignore the check
  • True to indicate the user is in a LTI session
  • False to indicate the user is not in a LTI session
  • “auto” to enable the check and take the information from the current session
update_user_stats(username, task, submission, result_str, grade, newsub)[source]

Update stats with a new submission

user_is_superadmin(username=None)[source]
Parameters:username – the username. If None, the username of the currently logged in user is taken
Returns:True if the user is superadmin, False else
user_saw_task(username, courseid, taskid)[source]

Set in the database that the user has viewed this task

Indices and tables