Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
Expand Down
24 changes: 15 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ keywords = [
"xblock",
]
dependencies = [
"Xblock[django]",
"Django>=4.2",
"django-crum",
"Django>=4.2",
"edx-codejail",
"openedx-filters",
"six"
"requests",
"six",
"Xblock[django]",
]

[project.urls]
Expand All @@ -47,14 +49,14 @@ dev = [
"edx-i18n-tools",
]
test = [
"edx-opaque-keys",
"pytest>=7.0",
"pytest-cov",
"pytest-django",
"coverage",
"edx-lint",
"edx-opaque-keys",
"edx-opaque-keys",
"mock",
"pytest-cov",
"pytest-django",
"pytest>=7.0",
"xblock-sdk",
]
docs = [
Expand All @@ -64,10 +66,11 @@ docs = [

[project.entry-points."xblock.v1"]
audio = "audio:AudioXBlock"
feedback = "feedback.feedback:FeedbackXBlock"
imagemodal = "imagemodal.xblocks:ImageModal"
qualtricssurvey = "qualtricssurvey.xblocks:QualtricsSurvey"
sql_grader = "sql_grader:SqlGrader"
submit-and-compare = "submit_and_compare.xblocks:SubmitAndCompareXBlock"
feedback = "feedback.feedback:FeedbackXBlock"

[project.entry-points."xblock.test.v0"]
feedbacktest = "feedback.feedbacktests:feedbacktests"
Expand All @@ -87,6 +90,8 @@ exclude = ["tests*", "*.tests", "*.tests.*"]

[tool.setuptools.package-data]
"*" = [
"conf/**/*",
"datasets/**/*",
"public/**/*",
"scenarios/**/*",
"static/**/*",
Expand Down Expand Up @@ -139,11 +144,12 @@ DJANGO_SETTINGS_MODULE = "feedback.settings.test"
branch = true
# Source paths will be added as xblocks are migrated
# Example: source = ["foo_xblock", "bar_xblock"]
source = []
source = ["sql_grader"]
omit = [
"*/tests/*",
"*/migrations/*",
"*/__pycache__/*",
"*/settings.py",
]

[tool.coverage.report]
Expand Down
164 changes: 164 additions & 0 deletions src/sql_grader/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
SQL Grader XBlock
=================

An XBlock for grading SQL statements via a SQLite engine.

Learners write SQL queries in a code editor with syntax highlighting and
autocomplete (Ctrl+Space). Their submission is compared against an
instructor-provided answer query; if the result sets match, the learner
receives full credit.

Built-in datasets
-----------------

The XBlock ships with two SQLite datasets ready to use:

* **rating** – ``Movie`` (mID, title, year, director), ``Reviewer`` (rID,
name), ``Rating`` (rID, mID, stars, ratingDate)
* **social** – ``Highschooler`` (ID, name, grade), ``Friend`` (ID1, ID2),
``Likes`` (ID1, ID2)

Custom datasets can be added as ``.sql`` files in ``sql_grader/datasets/``.


Installation
------------

System administrator
~~~~~~~~~~~~~~~~~~~~

Add the package to your Open edX requirements::

pip install xblocks-extra

Or install directly from the repository::

pip install "git+https://github.com/openedx/xblocks-extra.git#egg=xblocks-extra"


Course staff
~~~~~~~~~~~~

Go to **Settings → Advanced Settings → Advanced Module List** and add::

"sql_grader"


Codejail configuration
----------------------

The SQL grader executes learner SQL inside a codejail sandbox. Two execution
modes are supported:

Codejail-service REST API (Tutor / production)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Starting with the Sumac release, Tutor-based deployments use the
`codejail-service <https://github.com/openedx/codejail-service>`_ container
for sandboxed execution. When ``ENABLE_CODEJAIL_REST_SERVICE = True`` the
XBlock sends code to the service over HTTP instead of calling the local
codejail library.

For the sandbox to import ``sql_grader.problem``, the package must be
installed **inside the codejail-service sandbox venv** at image build time.
With the ``tutor-contrib-codejail`` plugin this is done via::

tutor config save --set \
"CODEJAIL_EXTRA_PIP_REQUIREMENTS=[\"git+https://github.com/openedx/xblocks-extra.git#egg=xblocks-extra\"]"

Then rebuild the codejail image::

tutor images build codejail

See `openedx-platform#36639 <https://github.com/openedx/openedx-platform/issues/36639>`_
for background on the migration from local codejail to the REST service.


Local codejail sandbox (native installs)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

When the REST service is **not** enabled, the XBlock falls back to calling
``codejail.safe_exec`` directly. This requires a properly configured local
codejail (sandbox user, AppArmor profile, ``CODE_JAIL.python_bin`` pointing to
a real Python binary). Refer to the
`codejail documentation <https://github.com/openedx/codejail>`_ for setup.


macOS / Docker Desktop note
~~~~~~~~~~~~~~~~~~~~~~~~~~~

AppArmor is not available on macOS. The codejail-service runs startup safety
checks that will fail without AppArmor, causing the service to reject
requests. For **local development only**, you can bypass this::

docker exec -u root <codejailservice-container> \
sed -i 's/STARTUP_SAFETY_CHECK_OK = None/STARTUP_SAFETY_CHECK_OK = True/' \
/app/codejail_service/startup_check.py
docker restart <codejailservice-container>

**Do not do this in production.**


Testing
-------

Studio (authoring)
~~~~~~~~~~~~~~~~~~

1. In Studio, create a new course or open an existing one.
2. Add an **Advanced** component and select **SQL Problem**.
3. In the component editor, set:

- **Dataset**: ``rating`` (or ``social``)
- **Answer Query**: e.g. ``SELECT title FROM Movie WHERE year > 2000``
- **Weight**: ``1``

4. Click **Save**.

LMS (learner experience)
~~~~~~~~~~~~~~~~~~~~~~~~~

1. Navigate to the unit containing the SQL Problem.
2. Enter a SQL query in the editor, e.g.::

SELECT title FROM Movie WHERE year > 2000

3. Click **Submit**. The XBlock compares your result set to the answer query:

- **Correct**: result sets match → full score
- **Incorrect**: result sets differ → zero score, you see your output vs.
expected

Example test queries (rating dataset)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. list-table::
:header-rows: 1
:widths: 60 40

* - Answer Query (set in Studio)
- Correct Submission
* - ``SELECT title FROM Movie WHERE year > 2000``
- ``SELECT title FROM Movie WHERE year > 2000``
* - ``SELECT title, year FROM Movie ORDER BY year``
- ``SELECT title, year FROM Movie ORDER BY year``
* - ``SELECT name FROM Reviewer WHERE rID IN (SELECT rID FROM Rating WHERE stars = 5)``
- ``SELECT name FROM Reviewer WHERE rID IN (SELECT rID FROM Rating WHERE stars = 5)``

To test a **wrong answer**, submit a different query — e.g. submit
``SELECT title FROM Movie WHERE year < 1990`` when the answer is
``SELECT title FROM Movie WHERE year > 2000``.

To test a **syntax error**, submit ``SELEKT title FROM Movie``.


Running unit tests
~~~~~~~~~~~~~~~~~~

From the ``xblocks-extra`` root::

make test

Or directly::

pytest src/sql_grader/
13 changes: 13 additions & 0 deletions src/sql_grader/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""
A SQL database graded component

Implemented in SQLite
"""

try:
from .xblocks import SqlGrader as SqlGrader
except Exception: # noqa: BLE001
# In the codejail sandbox Django is not configured, so XBlock field
# defaults (which use gettext_lazy) fail at import time. The sandbox
# only needs sql_grader.problem — this is safe to swallow.
pass
4 changes: 4 additions & 0 deletions src/sql_grader/conf/locale/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Configuration for i18n workflow.

locales:
- en # English - Source Language
65 changes: 65 additions & 0 deletions src/sql_grader/datasets/rating.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
https://s3-us-west-2.amazonaws.com/prod-c2g/db/Winter2013/files/rating.sql
https://s3-us-west-2.amazonaws.com/prod-c2g/db/Winter2013/files/viewmovie.sql
*/

/* Delete the tables if they already exist */
drop table if exists Movie;
drop table if exists Reviewer;
drop table if exists Rating;

/* Create the schema for our tables */
create table Movie(mID int, title text, year int, director text);
create table Reviewer(rID int, name text);
create table Rating(rID int, mID int, stars int, ratingDate date);

/* Populate the tables with our data */
insert into Movie values(101, 'Gone with the Wind', 1939, 'Victor Fleming');
insert into Movie values(102, 'Star Wars', 1977, 'George Lucas');
insert into Movie values(103, 'The Sound of Music', 1965, 'Robert Wise');
insert into Movie values(104, 'E.T.', 1982, 'Steven Spielberg');
insert into Movie values(105, 'Titanic', 1997, 'James Cameron');
insert into Movie values(106, 'Snow White', 1937, null);
insert into Movie values(107, 'Avatar', 2009, 'James Cameron');
insert into Movie values(108, 'Raiders of the Lost Ark', 1981, 'Steven Spielberg');

insert into Reviewer values(201, 'Sarah Martinez');
insert into Reviewer values(202, 'Daniel Lewis');
insert into Reviewer values(203, 'Brittany Harris');
insert into Reviewer values(204, 'Mike Anderson');
insert into Reviewer values(205, 'Chris Jackson');
insert into Reviewer values(206, 'Elizabeth Thomas');
insert into Reviewer values(207, 'James Cameron');
insert into Reviewer values(208, 'Ashley White');

insert into Rating values(201, 101, 2, '2011-01-22');
insert into Rating values(201, 101, 4, '2011-01-27');
insert into Rating values(202, 106, 4, null);
insert into Rating values(203, 103, 2, '2011-01-20');
insert into Rating values(203, 108, 4, '2011-01-12');
insert into Rating values(203, 108, 2, '2011-01-30');
insert into Rating values(204, 101, 3, '2011-01-09');
insert into Rating values(205, 103, 3, '2011-01-27');
insert into Rating values(205, 104, 2, '2011-01-22');
insert into Rating values(205, 108, 4, null);
insert into Rating values(206, 107, 3, '2011-01-15');
insert into Rating values(206, 106, 5, '2011-01-19');
insert into Rating values(207, 107, 5, '2011-01-20');
insert into Rating values(208, 104, 3, '2011-01-02');

/* Create the views */
create view LateRating as
select distinct R.mID, title, stars, ratingDate
from Rating R, Movie M
where R.mID = M.mID
and ratingDate > '2011-01-20';

create view HighlyRated as
select mID, title
from Movie
where mID in (select mID from Rating where stars > 3);

create view NoRating as
select mID, title
from Movie
where mID not in (select mID from Rating);
64 changes: 64 additions & 0 deletions src/sql_grader/datasets/social.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
https://s3-us-west-2.amazonaws.com/prod-c2g/db/Winter2013/files/social.sql
*/

/* Delete the tables if they already exist */
drop table if exists Highschooler;
drop table if exists Friend;
drop table if exists Likes;

/* Create the schema for our tables */
create table Highschooler(ID int, name text, grade int);
create table Friend(ID1 int, ID2 int);
create table Likes(ID1 int, ID2 int);

/* Populate the tables with our data */
insert into Highschooler values (1510, 'Jordan', 9);
insert into Highschooler values (1689, 'Gabriel', 9);
insert into Highschooler values (1381, 'Tiffany', 9);
insert into Highschooler values (1709, 'Cassandra', 9);
insert into Highschooler values (1101, 'Haley', 10);
insert into Highschooler values (1782, 'Andrew', 10);
insert into Highschooler values (1468, 'Kris', 10);
insert into Highschooler values (1641, 'Brittany', 10);
insert into Highschooler values (1247, 'Alexis', 11);
insert into Highschooler values (1316, 'Austin', 11);
insert into Highschooler values (1911, 'Gabriel', 11);
insert into Highschooler values (1501, 'Jessica', 11);
insert into Highschooler values (1304, 'Jordan', 12);
insert into Highschooler values (1025, 'John', 12);
insert into Highschooler values (1934, 'Kyle', 12);
insert into Highschooler values (1661, 'Logan', 12);

insert into Friend values (1510, 1381);
insert into Friend values (1510, 1689);
insert into Friend values (1689, 1709);
insert into Friend values (1381, 1247);
insert into Friend values (1709, 1247);
insert into Friend values (1689, 1782);
insert into Friend values (1782, 1468);
insert into Friend values (1782, 1316);
insert into Friend values (1782, 1304);
insert into Friend values (1468, 1101);
insert into Friend values (1468, 1641);
insert into Friend values (1101, 1641);
insert into Friend values (1247, 1911);
insert into Friend values (1247, 1501);
insert into Friend values (1911, 1501);
insert into Friend values (1501, 1934);
insert into Friend values (1316, 1934);
insert into Friend values (1934, 1304);
insert into Friend values (1304, 1661);
insert into Friend values (1661, 1025);
insert into Friend select ID2, ID1 from Friend;

insert into Likes values(1689, 1709);
insert into Likes values(1709, 1689);
insert into Likes values(1782, 1709);
insert into Likes values(1911, 1247);
insert into Likes values(1247, 1468);
insert into Likes values(1641, 1468);
insert into Likes values(1316, 1304);
insert into Likes values(1501, 1934);
insert into Likes values(1934, 1501);
insert into Likes values(1025, 1101);
Empty file.
Loading
Loading