Skip to content
......@@ -110,6 +110,12 @@ An Example
- Green
- ~CORRECT~ Yellow
external_resources:
-
title: Numpy
url: https://numpy.org/doc/
completion_text: |
# See you in class!
......@@ -121,41 +127,9 @@ Overall Structure of a Flow
When described in YAML, a flow has the following components:
.. class:: Flow
.. attribute:: title
A plain-text title of the flow
.. attribute:: description
A description in :ref:`markup` shown on the start page of the flow.
.. attribute:: completion_text
(Optional) Some text in :ref:`markup` shown once a student has
completed the flow.
.. attribute:: notify_on_submit
.. currentmodule:: course.content
(Optional) A list of email addresses which to notify about a flow submission by
a participant.
.. attribute:: rules
(Optional) Some rules governing students' use and grading of the flow.
See :ref:`flow-rules`.
.. attribute:: groups
A list of :class:`FlowPageGroup`. Exactly one of
:attr:`groups` or :class:`pages` must be given.
.. attribute:: pages
A list of :ref:`pages <flow-page>`. If you specify this, a single
:class:`FlowPageGroup` will be implicitly created. Exactly one of
:attr:`groups` or :class:`pages` must be given.
.. autoclass:: FlowDesc
.. _flow-rules:
......@@ -229,311 +203,36 @@ Here's a commented example:
Overall structure
^^^^^^^^^^^^^^^^^
.. class:: FlowRules
Found in the ``rules`` attribute of a :class:`Flow`.
.. attribute:: start
Rules that govern when a new session may be started and whether
existing sessions may be listed.
A list of :class:`FlowStartRules`
Rules are tested from top to bottom. The first rule
whose conditions apply determines the access.
.. attribute:: access
Rules that govern what a user may do while they are interacting with an
existing session.
A list of :class:`FlowAccessRules`.
Rules are tested from top to bottom. The first rule
whose conditions apply determines the access.
.. rubric:: Grading-Related
.. attribute:: grade_identifier
(Required) The identifier of the grade to be generated once the
participant completes the flow. If ``null``, no grade is generated.
.. attribute:: grade_aggregation_strategy
(Required if :attr:`grade_identifier` is not ``null``)
One of :class:`grade_aggregation_strategy`.
.. attribute:: grading
Rules that govern how (permanent) overall grades are generated from the
results of a flow. These rules apply once a flow session ends/is submitted
for grading. See :ref:`flow-life-cycle`.
(Required if grade_identifier is not ``null``)
A list of :class:`FlowGradingRules`
Rules are tested from top to bottom. The first rule
whose conditions apply determines the access.
.. autoclass:: FlowRulesDesc
Rules for starting new sessions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. class:: FlowStartRules
Rules that govern when a new session may be started and whether
existing sessions may be listed.
Found in the ``start`` attribute of :class:`FlowRules`.
.. rubric:: Conditions
.. attribute:: if_after
(Optional) A :ref:`datespec <datespec>` that determines a date/time after which this rule
applies.
.. attribute:: if_before
(Optional) A :ref:`datespec <datespec>` that determines a date/time before which this rule
applies.
.. attribute:: if_has_role
(Optional) A list of a subset of ``[unenrolled, ta, student, instructor]``.
.. attribute:: if_in_facility
(Optional) Name of a facility known to the RELATE web page. This rule allows
(for example) restricting flow starting based on whether a user is physically
located in a computer-based testing center (which RELATE can
recognize based on IP ranges).
.. attribute:: if_has_in_progress_session
(Optional) A Boolean (True/False) value, indicating that the rule only applies
if the participant has an in-progress session.
.. attribute:: if_has_session_tagged
(Optional) An identifier (or ``null``) indicating that the rule only applies
if the participant has a session with the corresponding tag.
.. attribute:: if_has_fewer_sessions_than
(Optional) An integer. The rule applies if the participant has fewer than this
number of sessions.
.. attribute:: if_has_fewer_tagged_sessions_than
(Optional) An integer. The rule applies if the participant has fewer than this
number of sessions with access rule tags.
.. attribute:: if_signed_in_with_matching_exam_ticket
(Optional) The rule applies if the participant signed in with an exam
ticket matching this flow.
.. rubric:: Rules specified
.. attribute:: may_start_new_session
(Mandatory) A Boolean (True/False) value indicating whether, if the rule applies,
the participant may start a new session.
.. attribute:: may_list_existing_sessions
(Mandatory) A Boolean (True/False) value indicating whether, if the rule applies,
the participant may view a list of existing sessions.
.. attribute:: tag_session
(Optional) An identifier that will be applied to a newly-created session as a "tag".
This can be used by :attr:`FlowAccessRules.if_has_tag` and
:attr:`FlowGradingRules.if_has_tag`.
.. attribute:: default_expiration_mode
(Optional) One of :class:`flow_session_expiration_mode`. The expiration mode applied
when a session is first created or rolled over.
.. autoclass:: FlowSessionStartRuleDesc
Rules about accessing and interacting with a flow
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. class:: FlowAccessRules
Rules that govern what a user may do with an existing session.
Found in the ``access`` attribute of :class:`FlowRules`.
.. rubric:: Conditions
.. attribute:: if_after
(Optional) A :ref:`datespec <datespec>` that determines a date/time after which this rule
applies.
.. attribute:: if_before
(Optional) A :ref:`datespec <datespec>` that determines a date/time before which this rule
applies.
.. attribute:: if_started_before
(Optional) A :ref:`datespec <datespec>`. Rule applies if the session was started before
this time.
.. attribute:: if_has_role
(Optional) A list of a subset of ``[unenrolled, ta, student, instructor]``.
.. attribute:: if_in_facility
(Optional) Name of a facility known to the RELATE web page. This rule allows
(for example) restricting flow access based on whether a user is physically
located in a computer-based testing center (which RELATE can
recognize based on IP ranges).
.. attribute:: if_has_tag
(Optional) Rule applies if session has this tag (see :attr:`FlowStartRules.tag_session`),
an identifier.
.. attribute:: if_in_progress
(Optional) A Boolean (True/False) value. Rule applies if the session's
in-progress status matches this Boolean value.
.. attribute:: if_completed_before
(Optional) A :ref:`datespec <datespec>`. Rule applies if the session was completed before
this time.
.. attribute:: if_expiration_mode
(Optional) One of :class:`flow_session_expiration_mode`. Rule applies if the expiration mode
(see :ref:`flow-life-cycle`) matches.
.. attribute:: if_session_duration_shorter_than_minutes
(Optional) The rule applies if the current session has been going on for
less than the specified number of minutes. Fractional values (e.g. "0.5")
are accepted here.
.. attribute:: if_signed_in_with_matching_exam_ticket
(Optional) The rule applies if the participant signed in with an exam
ticket matching this flow.
.. rubric:: Rules specified
.. attribute:: permissions
A list of :class:`flow_permission`.
:attr:`flow_permission.submit_answer` and :attr:`flow_permission.end_session`
are automatically removed from a finished (i.e. not 'in-progress')
session.
.. attribute:: message
(Optional) Some text in :ref:`markup` that is shown to the student in an 'alert'
box at the top of the page if this rule applies.
.. autoclass:: FlowSessionAccessRuleDesc
.. _flow-permissions:
Access permission bits
~~~~~~~~~~~~~~~~~~~~~~
.. currentmodule:: course.constants
.. autoclass:: flow_permission
Determining how final (overall) grades of flows are computed
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. class:: FlowGradingRules
Rules that govern how (permanent) grades are generated from the
results of a flow.
Found in the ``grading`` attribute of :class:`FlowRules`.
.. rubric:: Conditions
.. attribute:: if_has_role
(Optional) A list of a subset of ``[unenrolled, ta, student, instructor]``.
.. attribute:: if_started_before
(Optional) A :ref:`datespec <datespec>`. Rule applies if the session was started before
this time.
.. attribute:: if_has_tag
(Optional) Rule applies if session has this tag (see :attr:`FlowStartRules.tag_session`),
an identifier.
.. attribute:: if_completed_before
(Optional) A :ref:`datespec <datespec>`. Rule applies if the session was completed before
this time.
.. rubric:: Rules specified
.. attribute:: credit_percent
(Optional) A number indicating the percentage of credit assigned for this flow.
Defaults to 100 if not present.
.. attribute:: due
A :ref:`datespec <datespec>` indicating the due date of the flow. This is shown to the
participant and also used to batch-expire 'past-due' flows.
.. attribute:: generates_grade
(Optional) A Boolean indicating whether a grade will be recorded when this
flow is ended. Note that the value of this rule must never change over
the lifetime of a flow. I.e. a flow that, at some point during its lifetime,
*may* have been set to generate a grade must *always* be set to generate
a grade. Defaults to ``true``.
.. attribute:: use_last_activity_as_completion_time
.. currentmodule:: course.content
(Optional) A Boolean indicating whether the last time a participant made
a change to their flow should be used as the completion time.
.. autoclass:: FlowSessionGradingRuleDesc
Defaults to ``false`` to match past behavior. ``true`` is probably the more
sensible value for this.
.. attribute:: description
(Optional) A description of this set of grading rules being applied to the flow.
Shown to the participant on the flow start page.
.. attribute:: max_points
(Optional, an integer or floating point number if given)
The number of points on the flow which constitute
"100% of the achievable points". If not given, this is automatically
computed by summing point values from all constituent pages.
This may be used to 'grade out of N points', where N is a number that
is lower than the actually achievable count.
.. attribute:: max_points_enforced_cap
(Optional, an integer or floating point number if given)
No participant will have a grade higher than this recorded for this flow.
This may be used to limit the amount of 'extra credit' achieved beyond
:attr:`max_points`.
.. attribute:: bonus_points
(Optional, an integer or floating point number if given)
This number of points will be added to every participant's score.
.. currentmodule:: course.constants
.. autoclass:: grade_aggregation_strategy
......@@ -565,27 +264,9 @@ Each of these would be a separate 'group'.
Each group allows the following attributes:
.. class:: FlowPageGroup
.. currentmodule:: course.content
.. attribute:: id
(Required) A symbolic name for the page group.
.. attribute:: pages
(Required) A list of :ref:`flow-page`
.. attribute:: shuffle
(Optional) A boolean (True/False) indicating whether the order
of pages should be as in the list :attr:`FlowGroup.pages` or
determined by random shuffling
.. attribute:: max_page_count
(Optional) An integer limiting the page count of this group
to a certain value. Allows selection of a random subset by combining
with :attr:`FlowGroup.shuffle`.
.. autoclass:: FlowPageGroupDesc
.. _page-permissions:
......@@ -593,21 +274,22 @@ Per-page permissions
^^^^^^^^^^^^^^^^^^^^
The granted access permissions for the entire flow (see
:class:`FlowAccessRules`) can be modified on a per-page basis. This happens in
the ``access_rules`` sub-block of each page,
:class:`~course.content.FlowSessionAccessRuleDesc`) can be modified on a
per-page basis. This happens in the ``access_rules`` sub-block of each page,
e.g. in :attr:`course.page.ChoiceQuestion.access_rules`:
.. class:: PageAccessRules
.. attribute:: add_permissions
A list of :class:`flow_permission` values that are granted *in addition* to
the globally granted ones.
A list of :class:`~course.constants.flow_permission` values that are
granted *in addition* to the globally granted ones.
.. attribute:: remove_permissions
A list of :class:`flow_permission` values that are not granted for this page
even if they are granted by the global flow permissions.
A list of :class:`~course.constants.flow_permission` values that are
not granted for this page even if they are granted by the global flow
permissions.
For example, to grant permission to revise an answer on a
:class:`course.page.PythonCodeQuestion`, one might type::
......@@ -619,129 +301,74 @@ For example, to grant permission to revise an answer on a
- change_answer
value: 1
Predefined Page Types
---------------------
.. currentmodule:: course.page
The following page types are predefined:
* :class:`Page` -- a page of static text
* :class:`TextQuestion` -- a page allowing a textual answer
* :class:`SurveyTextQuestion` -- a page allowing an ungraded textual answer
* :class:`HumanGradedTextQuestion` -- a page allowing an textual answer graded by a human
* :class:`InlineMultiQuestion` -- a page allowing answers to be given in-line of a block of text
* :class:`ChoiceQuestion` -- a one-of-multiple-choice question
* :class:`MultipleChoiceQuestion` -- a many-of-multiple-choice question
* :class:`SurveyChoiceQuestion` -- a page allowing an ungraded multiple-choice answer
* :class:`PythonCodeQuestion` -- an autograded code question
* :class:`PythonCodeQuestionWithHumanTextFeedback`
-- a code question with automatic *and* human grading
* :class:`FileUploadQuestion`
-- a question allowing a file upload and human grading
.. _tabbed-page-view:
.. warning::
If you change the type of a question, you *must* also change its ID.
Otherwise, RELATE will assume that existing answer data for this
question applies to the new question type, and will likely get very
confused, for one because the answer data found will not be of the
expected type.
.. |id-page-attr| replace::
A short identifying name, unique within the page group. Alphanumeric
with dashes and underscores, no spaces.
.. |title-page-attr| replace::
The page's title, a string. No markup allowed. Required. If not supplied,
the first ten lines of the page body are searched for a
Markdown heading (``# My title``) and this heading is used as a title.
.. |access-rules-page-attr| replace::
Tabbed page view
^^^^^^^^^^^^^^^^^^^^
Optional. See :ref:`page-permissions`.
A flow page can be displayed in a tabbed view, where the first tab is the
flow page itself, and the subsequent tabs are additional external websites.
.. |value-page-attr| replace::
An example use case is when the participant does not have access to
browser-native tab functionality. This is the case when using the
"Guardian" browser with the "ProctorU" proctoring service.
An integer or a floating point number, representing the
point value of the question.
To access the tabbed page for a flow, append `/ext-resource-tabs` to the URL.
Alternatively, you can create a link to allow users to navigate to the tabbed
page directly. For example, `[Open tabs](ext-resource-tabs)`.
.. |text-widget-page-attr| replace::
You might need to set `X_FRAME_OPTIONS` in your Django settings to allow embedding
the flow page and external websites in iframes, depending on your site's configuration.
For example, you can add the following to your `local_settings.py`:
Optional.
One of ``text_input`` (default), ``textarea``, ``editor:MODE``
(where ``MODE`` is a valid language mode for the CodeMirror editor,
e.g. ``yaml``, or ``python`` or ``markdown``)
.. code-block:: python
Show a Page of Text/HTML (Ungraded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: Page()
X_FRAME_OPTIONS = 'SAMEORIGIN'
Fill-in-the-Blank (Automatically Graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: TextQuestion()
Free-Answer Survey (Ungraded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: SurveyTextQuestion()
.. autoclass:: TabDesc
Fill-in-the-Blank (long-/short-form) (Human-graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: HumanGradedTextQuestion()
Fill-in-Multiple-Blanks (Automatically Graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: InlineMultiQuestion()
.. _flow-life-cycle:
One-out-of-Many Choice (Automatically Graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: ChoiceQuestion()
Life cycle
----------
Many-out-of-Many Choice (Automatically Graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: MultipleChoiceQuestion()
.. currentmodule:: course.constants
One-out-of-Many Survey (Ungraded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: SurveyChoiceQuestion()
.. autoclass:: flow_session_expiration_mode
Write Python Code (Automatically Graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: PythonCodeQuestion()
.. _points-from-feedback:
Write Python Code (Automatically and Human-Graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: PythonCodeQuestionWithHumanTextFeedback()
Automatic point computation from textual feedback
-------------------------------------------------
Upload a File (Human-Graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you write your textual feedback in a certain way, Relate can help you compute
the grade (and update it when rubrics change):
.. autoclass:: FileUploadQuestion()
- Crossed all t's [pts:1/1 #cross_t]
- Dotted all i's [pts:2/2 #dot_i]
- Obeyed the axiom of choice [pts:1.5/1 #ax_choice]
Definining your own page types
------------------------------
The hash marks (and arbitrary identifiers after) are optional. If specified,
they will permit Relate to automatically update the grade feedback with
a new rubric (while maintaining point percentages for each item, as
found by the identifier).
.. autoclass:: PageContext
.. autoclass:: PageBehavior
.. autofunction:: get_auto_feedback
.. autoclass:: AnswerFeedback
.. autoclass:: PageBase
If at least one "denominator" is specified, Relate will automatically
compute the total and set the grade percentage. If no denominator
is specified anywhere, Relate will compute the sum and set the
point count.
.. currentmodule:: course.page.base
.. autoclass:: PageBaseWithTitle
.. autoclass:: PageBaseWithHumanTextFeedback
.. autoclass:: PageBaseWithCorrectAnswer
---
.. _flow-life-cycle:
If there is a line with three or more hyphens on its own, everything
after that line is kept unchanged when updating feedback from a rubric.
Life cycle
----------
- [pts:-1.5] Negative point contributions work, too.
.. currentmodule:: course.constants
.. note::
.. autoclass:: flow_session_expiration_mode
The feedback update facility is not currently implemented (but planned!).
Sample Rule Sets
----------------
......@@ -851,11 +478,11 @@ The rules for this can be written as follows::
if_has_role: [student, instructor]
if_after: exam 1 - 1 week
if_before: end:exam 1 + 2 weeks
permissions: [view, submit_answer, end_sesion, cannot_see_flow_result, lock_down_as_exam_session]
permissions: [view, submit_answer, end_session, cannot_see_flow_result, lock_down_as_exam_session]
-
if_has_role: [instructor]
permissions: [view, submit_answer, end_sesion, cannot_see_flow_result, lock_down_as_exam_session]
permissions: [view, submit_answer, end_session, cannot_see_flow_result, lock_down_as_exam_session]
-
permissions: []
......@@ -1063,7 +690,7 @@ The rules for this can be written as follows::
message: |
You have marked your session to roll over to 50% credit at the due
date. If you would like to have your current answers graded as-is
(and recieve full credit for them), please select 'End session
(and receive full credit for them), please select 'End session
and grade'.
permissions: [view, submit_answer, end_session, see_correctness, change_answer, set_roll_over_expiration_mode]
......
......@@ -16,7 +16,6 @@ features:
* Simple, text-based format for reusable course content
* Based on standard `YAML <https://en.wikipedia.org/wiki/YAML>`_,
`Markdown <https://en.wikipedia.org/wiki/Markdown>`_
* Instructors can implement custom question/page types in Python.
See `example content <https://github.com/inducer/relate-sample>`_.
......@@ -38,35 +37,30 @@ features:
* Facilitates live quizzes in the classroom.
* In-class instant messaging via XMPP.
Works well with `xmpp-popup <https://github.com/inducer/xmpp-popup>`_.
* Built-in support for `VideoJS <http://www.videojs.com/>`_ offers
easy-to-use support for integrating HTML5 video into course content
without the need for third-party content hosting.
RELATE is a based on the popular `Django <https://docs.djangoproject.com/>`_
web framework for Python. It lets students participate in online activities,
each of which is (generically) called a "flow", which allows a sequence of
pages, each of which can be both static or interactive content, for exapmle a
pages, each of which can be both static or interactive content, for example a
video, a quiz question, a page of text, or, within the confines of HTML,
something completely different.
Links
-----
More information around the web:
* `Documentation <http://documen.tician.de/relate>`_
* `Source code <https://github.com/inducer/relate>`_
Table of Contents
-----------------
.. toctree::
:maxdepth: 3
content.rst
flow.rst
faq.rst
misc.rst
:maxdepth: 2
content
flow
page-types
api
flow-page-api
faq
misc
🚀 Github <https://github.com/inducer/relate>
.. 💾 Download Releases <https://pypi.org/project/relate-courseware>
* :ref:`genindex`
Installation
============
RELATE currently works with Python 2.7 and Python 3. (By default, :file:`requirements.txt`
is set up for Python 3. See below for edit instructions if you are using Python 2.)
RELATE requires Python 3.
Install `bower <http://bower.io/>`_ and its dependencies, as described on its
web page.
Installation for Relate Development
-----------------------------------
(Optional) Make a virtualenv to install to::
Install `Node.js <https://nodejs.org>`__ and NPM.
virtualenv my-relate-env
source my-relate-env/bin/activate
Install `poetry <https://python-poetry.org>`__ to manage dependencies and virtual
environments::
To install, clone the repository::
curl -sSL https://install.python-poetry.org | python3 -
git clone git://github.com/inducer/relate
Note that this will put poetry in ``$HOME/.poetry/bin`` and modify your
``$HOME/.profile``. If you don't like that, see the
`poetry docs <https://python-poetry.org/docs/>`__ for alternate installation options.
Enter the relate directory::
To install, clone the repository and enter it::
git clone https://github.com/inducer/relate.git
cd relate
Edit :file:`requirements.txt` to choose a version of `dnspython`, then install
the dependencies::
Install the dependencies. Poetry will automatically create a virtualenv
(somewhere under ``$HOME/.poetry``) for this::
pip install -r requirements.txt
poetry install
If this installation step encounters hangs or errors that implicate access to a
keyring, setting a keyring backend may help::
export PYTHON_KEYRING_BACKEND=keyring.backends.fail.Keyring
Activate the virtual environment::
poetry shell
Copy (and, optionally, edit) the example configuration::
cp local_settings.example.py local_settings.py
vi local_settings.py
cp local_settings_example.py local_settings.py
$EDITOR local_settings.py
Initialize the database::
python manage.py migrate
python manage.py createsuperuser --username=$(whoami)
Retrieve static (JS/CSS) dependencies::
Retrieve frontend (JS/CSS) dependencies and build::
python manage.py bower_install
npm install
npm run build
Run the server::
python manage.py runserver
Open a browser to http://localhost:8000, sign in (your user name will be the
same as your system user name, or whatever `whoami` returned above) and select
same as your system user name, or whatever ``whoami`` returned above) and select
"Set up new course".
As you play with the web interface, you may notice that some long-running tasks
......@@ -53,16 +65,55 @@ those long-running tasks. Start a worker by running::
celery worker -A relate
.. note::
For Windows, you need first install `gevent` by::
pip install gevent
and then run::
celery worker -A relate -P gevent
See the `related issue <https://stackoverflow.com/a/47331438/3437454>`_ for more information.
To make this work, you also need a message broker running. This uses the
setting ``CELERY_BROKER_URL`` in ``local_settings.py`` and defaults to
``'amqp://'``. With that setting, you need for example `RabbitMQ
<https://www.rabbitmq.com/>`_ or another implementation installed. On
Debian-like Linux distributions (e.g. Ubuntu), the following should suffice::
apt-get install rabbitmq-server
.. note::
To install RabbitMQ for Windows, see `Installing on Windows
<https://www.rabbitmq.com/install-windows.html>`_ for more information.
See the `Celery documentation
<http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-broker_url>`_
for more information on alternate brokers and settings.
Note that, due to limitations of the demo configuration (i.e. due to not having
out-of-process caches available), long-running tasks can only show
"PENDING/STARTED/SUCCESS/FAILURE" as their progress, but no more detailed
information. This will be better as soon as you provide actual caches (the "CACHES"
option :file:`local_settings.py`).
Additional setup steps for Docker
---------------------------------
(TODO)
To allow running code questions, install docker and give Relate access. The simplest
way to do so is (on a Debian/Ubuntu system)::
apt install docker.io
Then add the user that runs Relate to the ``docker`` group in
:file:`/etc/group`. For deployment, this may be the ``www-data`` user.
You should also pull the default container image::
docker pull inducer/relate-runpy-amd64
Add to kernel command line, if needed::
......@@ -74,6 +125,8 @@ Change docker config to disallow IP forwarding::
in :file:`/etc/default/docker.io`.
If you need more scalable code execution, consider Docker Swarm.
Long-term maintenance
---------------------
......@@ -102,45 +155,52 @@ Setting up SAML2
- Edit :file:`saml_config.py` using :file:`saml_config.py.example`
as a guide.
How to translate RELATE
-----------------------
RELATE is translatable into languages other than English. Run the
following command::
django-admin makemessages -l de
Setting up Social Authentication (Google as an example)
-------------------------------------------------------
- Go to the `Google Developer Console <https://console.developers.google.com>`__.
- Create a project.
- Create an OAuth consent screen. You'll only need the ``.../auth/userinfo.email``
and ``.../auth/userinfo.profile`` scopes.
- Under "Credentials", create an OAuth 2.0 Client ID. Enter your equivalent of
``https://relate.cs.illinois.edu/social-auth/complete/google-oauth2/`` as
an authorized redirect URI. For testing, you can also add
``http://localhost:8000/social-auth/complete/google-oauth2/``.
You do not need any authorized JavaScript origins.
- Add ``"social_core.backends.google.GoogleOAuth2"`` to
``RELATE_SOCIAL_AUTH_BACKENDS``.
- Copy the Client ID into ``SOCIAL_AUTH_GOOGLE_OAUTH2_KEY``, and the
Client Secret from the developer console into ``SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET``.
- Restart your server. You should be good to go.
This will generate a message file for German, where the locale name ``de``
stands for Germany. The message file located in the ``locale`` directory
of your RELATE installation. For example, the above command will generate
a message file ``django.po`` in ``/project/root/locale/de/LC_MESSAGES``.
Deployment
----------
Edit ``django.po``. For each ``msgid`` string, put it's translation in
``msgstr`` right below. ``msgctxt`` strings, along with the commented
``Translators:`` strings above some ``msgid`` strings, are used to provide
more information for better understanding of the text to be translated.
A Simplified Chinese version (demo) of translation is included for Chinese
users, with locale name ``zh_CN``.
The following assumes you are using systemd on your deployment system.
Additional Setup Steps for Deploying to Production
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When translations are done, run the following command in root directory::
* Install nginx for reverse proxying and uwsgi to run the app server. See below
for configuration.
* Use postgres as a database. You need to create a user and a database that relate
will use and enter the details (database name, user name, password) into
:file:`local_settings.py`. You will also need to::
django-admin compilemessages -l de
poetry install -E postgres
Your translations are ready for use. If you translate RELATE, please submit
your translations for inclusion into the RELATE itself.
* The directory specified under ``GIT_ROOT`` must be owned by the user
running Relate.
To enable the translations, open your ``local_settings.py``, uncomment the
``LANGUAGE_CODE`` string and change 'en-us' to the locale name of your
language.
* Run::
For more instructions, please refer to `Localization: how to create
language files <https://docs.djangoproject.com/en/dev/topics/i18n/translation/#localization-how-to-create-language-files>`_.
./collectstatic.sh
Deployment
----------
to assemble the required collection of static files to be served, as the
production app server will not serve them (unlike the dev server).
The following assumes you are using systemd on your deployment system.
(Do not run ``python manage.py collectstatic`` directly; it will fail
because it cannot resolve some source map URLs in mathjax.)
Configuring uwsgi
^^^^^^^^^^^^^^^^^
......@@ -149,6 +209,7 @@ The following should be in :file:`/etc/uwsgi/apps-available/relate.ini`::
[uwsgi]
plugins = python
# or plugins = python3
socket = /tmp/uwsgi-relate.sock
chdir=/home/andreas/relate
virtualenv=/home/andreas/my-relate-env
......@@ -157,6 +218,7 @@ The following should be in :file:`/etc/uwsgi/apps-available/relate.ini`::
reload-mercy=8
max-requests=300
workers=8
autoload=false
Then run::
......@@ -225,8 +287,8 @@ Use a variant of this as :file:`/etc/systemd/system/relate-celery.service`::
ExecStartPre=/bin/mkdir -p /var/run/celery
ExecStartPre=/bin/chown -R www-data:www-data /var/run/celery/
ExecStart=/home/andreas/my-relate-env/bin/celery multi start worker \
-A relate --pidfile=/var/run/celery/celery.pid \
ExecStart=/home/andreas/my-relate-env/bin/celery -A relate multi start worker \
--pidfile=/var/run/celery/celery.pid \
--logfile=/var/log/celery/celery.log --loglevel="INFO"
ExecStop=/home/andreas/my-relate-env/bin/celery multi stopwait worker \
--pidfile=/var/run/celery/celery.pid
......@@ -247,12 +309,164 @@ Then run::
# systemctl status relate-celery.service
# systemctl enable relate-celery.service
Tips
====
Minimal Install for Validating Course Content
---------------------------------------------
Install poetry::
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 -
See the `Poetry documentation <https://python-poetry.org/docs/>`__ for other options.
Then, download relate::
git clone https://github.com/inducer/relate.git
cd relate
Poetry creates virtualenvs in your home directory by default. Create a file ``poetry.toml``
with the following contents::
[virtualenvs]
in-project = true
Next, install Relate and its dependencies::
poetry install
In order to use the ``relate`` command, you need to activate the virtualenv that
was created::
source ~/path/to/relate/checkout/.venv/bin/activate
Enabling I18n support/Translating RELATE into other Languages
=============================================================
Creating New Translations
-------------------------
RELATE is translatable into languages other than English. Run the
following command::
django-admin makemessages -l de
This will generate a message file for German, where the locale name ``de``
stands for Germany. The message file located in the ``locale`` directory
of your RELATE installation. For example, the above command will generate
a message file ``django.po`` in ``/project/root/locale/de/LC_MESSAGES``.
Edit ``django.po``. For each ``msgid`` string, put it's translation in
``msgstr`` right below. ``msgctxt`` strings, along with the commented
``Translators:`` strings above some ``msgid`` strings, are used to provide
more information for better understanding of the text to be translated.
A Simplified Chinese version (demo) of translation is included for Chinese
users, with locale name ``zh_HANS``.
Enabling Translations
---------------------
When translations are done, run the following command in root directory::
django-admin compilemessages -l de
Your translations are ready for use. If you translate RELATE, please submit
your translations for inclusion into the RELATE itself.
To enable the translations, open your ``local_settings.py``, uncomment the
``LANGUAGE_CODE`` string and change 'en-us' to the locale name of your
language.
For more instructions, please refer to `Localization: how to create
language files <https://docs.djangoproject.com/en/dev/topics/i18n/translation/#localization-how-to-create-language-files>`_.
.. _cli:
Installing the Command Line Interface
-------------------------------------
RELATE validation (and a number of other functionalities) are also via the
:command:`relate` command. This may be installed as follows.
Install `poetry <https://python-poetry.org>`__ to manage dependencies and virtual
environments::
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3
Clone the relate repository and enter it::
git clone https://github.com/inducer/relate.git
cd relate
Create a file ``poetry.toml`` containing the lines::
[virtualenvs]
in-project = true
and running::
poetry install --no-dev
in the root directory of the RELATE distribution. The ``relate`` command is
then available at ``relate/.venv/bin/relate`` and can be used in a course
repository by running::
relate validate .
A number of additional functionalities (such as ``relate test-code``) are
also available from the ``relate`` command.
User-visible Changes
====================
Version 2022.1
--------------
* In March 2022 (specifically, with
`this pull request <https://github.com/inducer/relate/pull/892>`__),
Relate adopted Bootstrap 5, which brought with it some changes that might
affect courses that relied on CSS or other markup features specific to
Bootstrap 3. For comprehensive advice on how to port your content to
the upgraded CSS framework, see the `official porting guide
<https://getbootstrap.com/docs/4.6/migration/>`__. Here are some specific
tips on migrating your course content that may suffice for simple cases:
* The CSS class ``btn-default`` was removed. Use ``btn-secondary`` instead.
Potentially consider the new ``btn-outline-{primary,secondary}``.
* If you have collapsing panels in your course content, you may use markup
like the following instead:
.. code:: html
<div class="card mb-3" markdown="block">
<div class="card-header">
<h5 class="card-title dropdown-toggle">
<a class="text-decoration-none link-secondary"
data-bs-toggle="collapse" href="#starter-code" aria-expanded="false" aria-controls="starter-code">
Header
</a>
</h5>
</div>
<div id="starter-code" class="collapse">
<div class="card-body">
Content
</div>
</div>
</div>
If you are looking for an updated version of the ``collapsible`` macro from
the sample content, you may find it `here
<https://github.com/inducer/relate-sample/blob/0a7019584fda7ea0b91cc3fd370b799df249460a/content-macros.jinja#L18-L34>`__.
* Relate has also dropped "Font Awesome" (which is no longer maintained in
open-source form) in favor of `Bootstrap Icons
<https://icons.getbootstrap.com/>`__, which provides a similar icons with a
look consistent with Bootstrap. In many cases, all that is required is
to switch ``fa fa-key`` CSS classes to ``bi bi-key`` (or similar).
See the full list of available icons `here <https://icons.getbootstrap.com/>`__.
* Relate can now automatically compute point counts/percentages from
human-provided feedback. See :ref:`points-from-feedback`.
Version 2015.1
--------------
......@@ -261,27 +475,4 @@ First public release.
License
=======
RELATE is licensed to you under the MIT/X Consortium license:
Copyright (c) 2014-15 Andreas Klöckner and Contributors.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
.. include:: ../LICENSE
Predefined Page Types
---------------------
.. currentmodule:: course.page
The following page types are predefined:
* :class:`Page` -- a page of static text
* :class:`TextQuestion` -- a page allowing a textual answer
* :class:`SurveyTextQuestion` -- a page allowing an ungraded textual answer
* :class:`HumanGradedTextQuestion` -- a page allowing an textual answer graded by a human
* :class:`InlineMultiQuestion` -- a page allowing answers to be given in-line of a block of text
* :class:`ChoiceQuestion` -- a one-of-multiple-choice question
* :class:`MultipleChoiceQuestion` -- a many-of-multiple-choice question
* :class:`SurveyChoiceQuestion` -- a page allowing an ungraded multiple-choice answer
* :class:`PythonCodeQuestion` -- an autograded code question
* :class:`PythonCodeQuestionWithHumanTextFeedback`
-- a code question with automatic *and* human grading
* :class:`FileUploadQuestion`
-- a question allowing a file upload and human grading
.. warning::
If you change the type of a question, you *must* also change its ID.
Otherwise, RELATE will assume that existing answer data for this
question applies to the new question type, and will likely get very
confused, for one because the answer data found will not be of the
expected type.
.. |id-page-attr| replace::
A short identifying name, unique within the page group. Alphanumeric
with underscores, no spaces.
.. |title-page-attr| replace::
The page's title, a string. No markup allowed. Required. If not supplied,
the first ten lines of the page body are searched for a
Markdown heading (``# My title``) and this heading is used as a title.
.. |access-rules-page-attr| replace::
Optional. See :ref:`page-permissions`.
.. |value-page-attr| replace::
An integer or a floating point number, representing the
point value of the question.
.. |is-optional-page-attr| replace::
Optional. A Boolean value indicating whether the page is an optional page
which does not require answer for full completion of the flow. If
``true``, the attribute *value* should not present. Defaults to ``false``
if not present. Note that ``is_optional_page: true`` differs from ``value:
0`` in that finishing flows with unanswered page(s) with the latter will be
warned of "unanswered question(s)", while with the former won't. When using
not-for-grading page(s) to collect answers from students, it's to better
use ``value: 0``.
.. |text-widget-page-attr| replace::
Optional.
One of ``text_input`` (default), ``textarea``, ``editor:MODE``
(where ``MODE`` is a valid language mode for the CodeMirror editor,
e.g. ``yaml``, or ``python`` or ``markdown``)
Show a Page of Text/HTML (Ungraded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: Page()
Fill-in-the-Blank (Automatically Graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: TextQuestion()
Free-Answer Survey (Ungraded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: SurveyTextQuestion()
Fill-in-the-Blank (long-/short-form) (Human-graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: HumanGradedTextQuestion()
Fill-in-the-Blank (long-form, with formatting) (Human-graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: HumanGradedRichTextQuestion()
Fill-in-Multiple-Blanks (Automatically Graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: InlineMultiQuestion()
One-out-of-Many Choice (Automatically Graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: ChoiceQuestion()
Many-out-of-Many Choice (Automatically Graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: MultipleChoiceQuestion()
One-out-of-Many Survey (Ungraded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: SurveyChoiceQuestion()
Write Python Code (Automatically Graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: PythonCodeQuestion()
Write Python Code (Automatically and Human-Graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: PythonCodeQuestionWithHumanTextFeedback()
Upload a File (Human-Graded)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: FileUploadQuestion()
FROM inducer/debian-i386
FROM debian:testing-slim
MAINTAINER Andreas Kloeckner <inform@tiker.net>
EXPOSE 9941
RUN useradd runpy
ARG RLCONTAINER
RUN useradd runcode
RUN echo "---------------- BUILD VARIANT: $RLCONTAINER"
# Temporarily needed for pandas
RUN echo "deb http://httpredir.debian.org/debian unstable main contrib" >> /etc/apt/sources.list
RUN echo 'APT::Default-Release "testing";' >> /etc/apt/apt.conf
RUN apt-get update
RUN apt-get -y -o APT::Install-Recommends=0 -o APT::Install-Suggests=0 install python3-scipy python3-pip python3-matplotlib python3-pillow graphviz python3-pandas python3-sympy
RUN apt-get -y -o APT::Install-Recommends=0 -o APT::Install-Suggests=0 install \
python3-scipy \
python3-numpy \
python3-pip \
python3-matplotlib \
python3-pillow \
graphviz \
python3-pandas \
python3-statsmodels \
python3-skimage \
python3-sympy \
python3-pip \
python3-dev \
python3-setuptools \
python3-cffi \
g++
RUN if [ "$RLCONTAINER" = "full" ] ; then apt-get -o APT::Install-Recommends=0 -o APT::Install-Suggests=0 -y install \
"pocl-opencl-icd" \
"libpocl2" \
"libpocl2-common" \
"ocl-icd-libopencl1" \
"python3-pyopencl" \
git; \
fi
RUN if [ "$RLCONTAINER" = "full" ] ; then python3 -m pip install --break-system-packages git+https://github.com/inducer/pymbolic.git; fi
RUN if [ "$RLCONTAINER" = "full" ] ; then python3 -m pip install --break-system-packages git+https://github.com/inducer/loopy.git; fi
RUN apt-get clean
RUN fc-cache
RUN mkdir -p /opt/runpy
ADD runpy /opt/runpy/
COPY code_feedback.py /opt/runpy/
COPY code_runpy_backend.py /opt/runpy/
RUN mkdir -p /opt/runcode
ADD runcode /opt/runcode/
COPY code_feedback.py /opt/runcode/
COPY code_run_backend.py /opt/runcode/
RUN ls /opt/runcode
RUN sed -i s/TkAgg/Agg/ /etc/matplotlibrc
RUN echo "savefig.dpi : 80" >> /etc/matplotlibrc
RUN echo "savefig.bbox : tight" >> /etc/matplotlibrc
# RUN if [ "$RLCONTAINER" = "full" ] ; then pip3 install --upgrade tensorflow; fi
RUN if [ "$RLCONTAINER" = "full" ] ; then pip3 install --break-system-packages --upgrade jax; fi
RUN rm -Rf /root/.cache
# may use ./flatten-container.sh to reduce disk space
#! /bin/sh
cp ../course/page/code_feedback.py ../course/page/code_runpy_backend.py .
docker build .
rm code_feedback.py code_runpy_backend.py
if test "$1" = "-f"; then
RLCONTAINER=full
IMGNAME=inducer/relate-runcode-python-amd64-full
else
RLCONTAINER=base
IMGNAME=inducer/relate-runcode-python-amd64
fi
cp ../course/page/code_feedback.py .
cp ../course/page/code_run_backend.py .
docker build --no-cache --build-arg RLCONTAINER="$RLCONTAINER" . -t $IMGNAME
rm code_feedback.py code_run_backend.py
#! /bin/bash
if test "$1" = ""; then
echo "$0 imagename"
exit 1
fi
CONTAINER=$(docker create "$1")
docker export "$CONTAINER" | \
docker import \
-c "EXPOSE 9941" \
-
docker rm -f $CONTAINER
......@@ -29,7 +29,21 @@ import socketserver
import json
import sys
import io
from code_runpy_backend import Struct, run_code, package_exception
try:
from code_run_backend import Struct, run_code, package_exception
except ImportError:
try:
# When faking a container for unittest
from course.page.code_run_backend import (
Struct, run_code, package_exception)
except ImportError:
# When debugging, i.e., run "python runpy" command line
import os
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), os.pardir)))
from course.page.code_run_backend import (
Struct, run_code, package_exception)
from http.server import BaseHTTPRequestHandler
PORT = 9941
......@@ -40,7 +54,9 @@ TEST_COUNT = 0
def truncate_if_long(s):
if len(s) > OUTPUT_LENGTH_LIMIT:
s = s[:OUTPUT_LENGTH_LIMIT] + "[TRUNCATED... TOO MUCH OUTPUT]"
s = (s[:OUTPUT_LENGTH_LIMIT//2]
+ "\n[... TOO MUCH OUTPUT, SKIPPING ...]\n"
+ s[-OUTPUT_LENGTH_LIMIT//2:])
return s
......
from __future__ import annotations
def main():
from course.page import request_python_run
from course.page.code import request_run
req = {
"setup_code": "a,b=5,2",
......@@ -11,13 +14,13 @@ def main():
}
count = 0
while True:
print count
print(count)
count += 1
res = request_python_run(req, 5,
image="inducer/runpy-i386")
res = request_run(req, 10)
if res["result"] != "success":
print res
print(res)
break
if __name__ == "__main__":
main()
......@@ -2,14 +2,18 @@
from __future__ import print_function
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "relate.settings")
import django
django.setup()
import sys
from django.contrib.sessions.models import Session
from accounts.models import User
import sys
session_key = sys.argv[1]
......
# -*- encoding: utf-8 -*-
from __future__ import unicode_literals
"""
LANG_INFO is a dictionary structure to provide meta information about languages.
About name_local: capitalize it as if your language name was appearing
inside a sentence in your language.
The 'fallback' key can be used to specify a special fallback logic which doesn't
follow the traditional 'fr-ca' -> 'fr' fallback logic.
"""
LANG_INFO = {
'zh-cn': {
'fallback': ['zh-hans'],
'bidi': False,
'code': 'zh-cn',
'name': 'Simplified Chinese',
'name_local': '简体中文',
},
'zh-tw': {
'fallback': ['zh-hant'],
'bidi': False,
'code': 'zh-tw',
'name': 'Tranditional Chinese',
'name_local': '繁体中文',
},
'zh-hk': {
'fallback': ['zh-hant'],
'bidi': False,
'code': 'zh-tw',
'name': 'Tranditional Chinese',
'name_local': '繁体中文',
},
}
@import "jstree/dist/themes/default/style";
@import "video.js/dist/video-js";
$color-mode-type: media-query;
@import "node_modules/bootstrap/scss/bootstrap.scss";
@import "node_modules/select2/dist/css/select2";
// work around spurious extra quoting in woff include
$bootstrap-icons-font-file: "./fonts/bootstrap-icons";
@import 'node_modules/bootstrap-icons/font/bootstrap-icons.scss';
@import "./style.scss";
// Ideally, these should be separate, but they use Bootstrap classes.
@import "./codemirror.scss";
@import "./prosemirror.scss";
.cm-editor .cm-gutters {
@extend .border-end;
@extend .border-secondary-subtle;
}
.cm-editor .cm-gutter {
@extend .bg-secondary-subtle;
@extend .text-secondary-emphasis;
}
.cm-editor .cm-activeLineGutter {
@extend .bg-secondary;
@extend .text-white;
background-color: yellow;
}
.cm-editor .cm-panels {
@extend .bg-body-secondary;
@extend .text-body;
}
.cm-editor .cm-selectionBackground {
@extend .bg-body-secondary;
}
/* Based on https://discuss.codemirror.net/t/dynamic-light-mode-dark-mode-how/4709/5 */
.cm-editor, .cm-view {
.cmt-atom {color: #221199;}
.cmt-comment {color: #AA5500;}
.cmt-keyword {color: #8959A8;}
.cmt-literal {color: #4271AE;}
.cmt-number {color: #F5871F;}
.cmt-operator {color: #008803;}
.cmt-separator {color: #990033;}
.cmt-string {color: #FF5500;}
@media (prefers-color-scheme: dark) {
.cmt-atom {color: #F78C6C;}
.cmt-comment {color: #A0A0A0;}
.cmt-keyword {color: #C792EA;}
.cmt-literal {color: #FFCB6B;}
.cmt-number {color: #FF5370;}
.cmt-operator {color: #89DDFF;}
.cmt-separator {color: #FF7DE9;}
.cmt-string {color: #F07178;}
}
}
.rl-prosemirror-container {
@extend .border;
@extend .border-2;
}
.rl-prosemirror-container .ProseMirror {
margin-left: 1ex;
margin-right: 1ex;
margin-top: 2px;
margin-bottom: 2px;
}
.rl-prosemirror-container .ProseMirror:read-write:focus {
outline: none;
}
.ProseMirror > p:last-child { margin-bottom: 0 !important;}
.ProseMirror-menubar {
@extend .bg-secondary-subtle;
@extend .border-bottom;
@extend .border-secondary-subtle;
}
.ProseMirror-menuseparator {
@extend .border-secondary-subtle;
}
.ProseMirror-menu-submenu, .ProseMirror-menu-dropdown-menu {
@extend .bg-secondary-subtle;
}
math-display.ProseMirror-selectednode {
@extend .bg-secondary-subtle;
}
.well .start-well-title {
margin-top:0;
}
table.past-flow-session-table th {
margin-right:1em;
border-bottom: 1px solid black;
@extend .border-bottom;
@extend .border-secondary-subtle;
}
table.past-flow-session-table td {
......@@ -25,60 +27,51 @@ table.past-flow-session-table {
/* bootstrap padding of well is 19px == (13+6)px */
.flow-well {
padding-top: 13px;
}
.flow-nav {
float:left;
margin-top: 6px;
}
.relate-flow-submit {
float: right;
margin-top: 6px;
}
.relate-flow-page-menu {
float: right;
margin-top: 6px;
}
.relate-flow-page-send-email {
float: right;
margin-top: 6px;
}
.relate-flow-page-past-submissions {
float: right;
margin-top: 6px;
}
.flow-session-expiration-panel {
float:right;
white-space: nowrap;
margin-right: 1em;
margin-top: 6px;
}
.flow-session-expiration-panel .form-control {
width: auto;
display: inline;
}
code {
/* Bootstrap: Hot pink for code? Really? */
color: #333;
/* styling tooltip arrow for inline question validation errors */
/* ref: http://stackoverflow.com/a/38279489/3437454*/
.tooltip-danger.tooltip-inner {
text-align: left;
background-color: #a94442;
}
[data-placement="bottom"] + .tooltip > .tooltip-danger.tooltip-arrow {
border-bottom-color: #a94442;
}
.relate-flow-page-toc {
margin-top: 6px;
padding-left: 15px;
padding-right: 15px;
float: left;
text-align: center;
@extend .px-2;
}
@include media-breakpoint-down(md) {
.relate-flow-page-toc {
clear: both;
}
}
.relate-flow-page-bookmark-button {
@extend .btn-outline-secondary;
}
.relate-flow-page-bookmark-button.relate-bookmarked {
background-color: #ff8;
}
......@@ -120,6 +113,11 @@ code {
border-radius: 4px;
}
/* remove extra spacing from ChoiceQuestion options */
label div.relate-markup p:only-child {
margin-bottom: 0;
}
/* }}} */
/* {{{ histograms */
......@@ -191,6 +189,10 @@ div.progress:hover span.stats-percentage{
bottom: 0;
overflow-y: auto;
}
.sandbox-page-editor .cm-editor {
max-height: calc(100vh - 45ex);
}
.sandbox-page-preview {
position: absolute;
top: 60px;
......@@ -202,26 +204,48 @@ div.progress:hover span.stats-percentage{
padding-right:50px;
}
.grading-page-student-work {
position: absolute;
top: 60px;
left: 0;
width: 50%;
bottom: 0;
padding-left:50px;
padding-right:50px;
overflow-y: auto;
}
.grading-page-grade-entry {
position: absolute;
top: 60px;
right: 0;
width: 50%;
bottom: 0;
overflow-y: auto;
padding-left:50px;
padding-right:50px;
@media (min-width: 768px) {
.grading-page-student-work {
position: absolute;
top: 60px;
left: 0;
width: 50%;
bottom: 0;
padding-left: 50px;
padding-right: 50px;
overflow-y: auto;
}
.grading-page-grade-entry {
position: absolute;
top: 60px;
right: 0;
width: 50%;
bottom: 0;
overflow-y: auto;
padding-left: 50px;
padding-right: 50px;
}
}
@media (max-width: 767px) {
.grading-page-student-work.container > div {
margin-left: 5px;
margin-right: 5px;
}
.grading-page-grade-entry.container > div {
margin-left: 5px;
margin-right: 5px;
}
h1, .h1 {
font-size: 30px;
}
h2, .h2 {
font-size: 24px;
}
}
.relate-participation-active {
......@@ -245,30 +269,7 @@ div.progress:hover span.stats-percentage{
color: #7cf;
}
/* {{{ codemirror */
.sandbox-page-editor .CodeMirror {
height:500px;
}
.cm-trailingspace {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QUXCToH00Y1UgAAACFJREFUCNdjPMDBUc/AwNDAAAFMTAwMDA0OP34wQgX/AQBYgwYEx4f9lQAAAABJRU5ErkJggg==);
background-position: bottom left;
background-repeat: repeat-x;
}
.cm-tab {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAYAAAAkuj5RAAAAAXNSR0IArs4c6QAAAGFJREFUSMft1LsRQFAQheHPowAKoACx3IgEKtaEHujDjORSgWTH/ZOdnZOcM/sgk/kFFWY0qV8foQwS4MKBCS3qR6ixBJvElOobYAtivseIE120FaowJPN75GMu8j/LfMwNjh4HUpwg4LUAAAAASUVORK5CYII=);
background-position: right;
background-repeat: no-repeat;
}
.CodeMirror.cm-s-relate-readonly {
background-color: #e8e8e8;
}
/* }}} */
.relate-btn-xs-vert-spaced {
.relate-btn-sm-vert-spaced {
margin-bottom: 3px;
}
......@@ -300,4 +301,49 @@ a .sensitive {
text-decoration: none;
}
/* vim: set foldmethod=marker: */
/* if not using "markdown.extensions.codehilite" extension */
div.cell.border-box-sizing.code_cell.rendered > div.input > div.inner_cell > div.input_area > pre {
margin: 0;
}
div.rendered_html div.highlight {
border: 1px solid #cfcfcf;
border-radius: 2px;
background: #f7f7f7;
}
div.rendered_html div.highlight > pre {
margin: 0.4em;
background-color: transparent;
}
/* ipython notebook background color as white for version 5.4 */
.relate-notebook-container {
background-color: #ffffff !important;
}
.relate-dropdown-menu {
@extend .mx-2;
@extend .dropdown;
> a {
@extend .link-secondary;
@extend .text-decoration-none;
@extend .dropdown-toggle;
}
> ul {
/* @extend .dropdown-menu; */ /* spelled out in HTML, needed for popper? */
@extend .shadow;
}
}
.relate-well {
@extend .card;
/*
@extend .text-dark;
@extend .bg-light;
*/
@extend .p-3;
@extend .mb-3;
}
/* vim: set foldmethod=marker:sw=2 */
import './base';
import 'jstree';
import 'video.js';
import '../css/base-with-markup.css';
window.MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']],
},
options: {
processHtmlClass: 'relate-markup',
},
};
import './jquery-importer';
import jQuery from 'jquery';
import * as bootstrap from 'bootstrap';
import select2 from 'select2';
import * as rlUtils from './rlUtils';
import * as bsUtils from './bsUtils';
import 'htmx.org';
import '../css/base.scss';
select2(jQuery);
document.addEventListener('DOMContentLoaded', () => {
// document.body is not available until the DOM is loaded.
document.body.addEventListener('htmx:responseError', (evt) => {
bsUtils.showToast(
`HTMX request failed: ${evt.detail.xhr.status}:
${evt.detail.xhr.statusText}
(${evt.detail.xhr.responseURL})`,
);
});
});
globalThis.rlUtils = rlUtils;
globalThis.bsUtils = bsUtils;
globalThis.bootstrap = bootstrap;
import * as bootstrap from 'bootstrap';
/* eslint-disable-next-line import/prefer-default-export */
export function showToast(msg, title) {
const errorToast = document.getElementById('relate-ui-toast');
document.getElementById('relate-ui-toast-body').innerHTML = msg;
if (title) {
document.getElementById('relate-ui-toast-title').innerHTML = title;
}
const toast = new bootstrap.Toast(errorToast);
toast.show();
}