Skip to content
Draft
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
11 changes: 9 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ keywords = [
dependencies = [
"XBlock",
"Django>=4.2",
"edx-codejail",
]

[project.urls]
Expand All @@ -46,6 +47,7 @@ test = [
"pytest>=7.0",
"pytest-cov",
"pytest-django",
"edx-opaque-keys",
]
docs = [
"sphinx",
Expand All @@ -54,6 +56,7 @@ docs = [

[project.entry-points."xblock.v1"]
audio = "audio:AudioXBlock"
sql_grader = "sql_grader:SqlGrader"

# Packages live in src/ but are installed without the src prefix
# e.g., src/foo_xblock/ is installed as foo_xblock
Expand All @@ -67,6 +70,9 @@ exclude = ["tests*"]

[tool.setuptools.package-data]
"*" = [
"conf/**/*",
"datasets/**/*",
"scenarios/**/*",
"static/**/*",
"templates/**/*",
"translations/**/*",
Expand Down Expand Up @@ -107,7 +113,7 @@ indent-style = "space"
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
testpaths = ["tests"]
testpaths = ["tests", "src"]
addopts = "-v --tb=short"

# Coverage configuration
Expand All @@ -116,11 +122,12 @@ addopts = "-v --tb=short"
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
41 changes: 41 additions & 0 deletions src/sql_grader/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
SQL Grader XBlock
=================

XBlock to grade SQL statements
via a SQLite engine.


This package provides an XBlock for use with the Open EdX Platform.
Participants can be graded on SQL scripts, written in code editor
supporting:

- syntax highlighting
- autocomplete (with Ctrl+Space)



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


System Administrator
~~~~~~~~~~~~~~~~~~~~

To install the XBlock on your platform,
add the following to your `requirements.txt` file:

xblocks-extra



Course Staff
~~~~~~~~~~~~

To install the XBlock in your course,
access your `Advanced Module List`:

Settings -> Advanced Settings -> Advanced Module List

and add the following:

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

Implemented in SQLite
"""

from .xblocks import SqlGrader as SqlGrader
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.
34 changes: 34 additions & 0 deletions src/sql_grader/mixins/dates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
Mixin date-related functionality
"""

import datetime


class EnforceDueDates:
"""
xBlock Mixin to allow xblocks to check the due date
(taking the graceperiod into account) of the
subsection in which they are placed
"""

def is_past_due(self):
"""
Determine if component is past-due
"""
# These values are pulled from platform.
# They are defaulted to None for tests.
due = getattr(self, "due", None)
graceperiod = getattr(self, "graceperiod", None)
# Calculate the current DateTime so we can compare the due date to it.
# datetime.utcnow() returns timezone naive date object.
now = datetime.datetime.utcnow()
if due is not None:
# Remove timezone information from platform provided due date.
# Dates are stored as UTC timezone aware objects on platform.
due = due.replace(tzinfo=None)
if graceperiod is not None:
# Compare the datetime objects (both have to be timezone naive)
due = due + graceperiod
return now > due
return False
94 changes: 94 additions & 0 deletions src/sql_grader/mixins/fragment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""
Mixin fragment/html behavior into XBlocks

Note: We should resume test coverage for all lines in this file once
split into its own library.
"""

from web_fragments.fragment import Fragment
from xblock.core import XBlock


class XBlockFragmentBuilderMixin:
"""
Create a default XBlock fragment builder
"""

static_css = [
"view.css",
]
static_js = [
"view.js",
]
static_js_init = None
template = "view.html"

def provide_context(self, context): # pragma: no cover
"""
Build a context dictionary to render the student view

This should generally be overriden by child classes.
"""
context = context or {}
context = dict(context)
return context

@XBlock.supports("multi_device")
def student_view(self, context=None):
"""
Build the fragment for the default student view
"""
template = self.template
context = self.provide_context(context)
static_css = self.static_css or []
static_js = self.static_js or []
js_init = self.static_js_init
fragment = self.build_fragment(
template=template,
context=context,
css_files=static_css,
js_files=static_js,
js_init=js_init,
)
return fragment

# pylint: disable=too-many-arguments,too-many-positional-arguments
def build_fragment(
self,
template="",
context=None,
css_files=None,
js_files=None,
js_init=None,
):
"""
Creates a fragment for display.
"""
context = context or {}
css_files = css_files or []
js_files = js_files or []
rendered_template = ""
if template: # pragma: no cover
template = "templates/" + template
rendered_template = self.loader.render_django_template(
template,
context=context,
i18n_service=self.runtime.service(self, "i18n"),
)
fragment = Fragment(rendered_template)
for item in css_files:
if item.startswith("/"):
url = item
else:
item = "static/" + item
url = self.runtime.local_resource_url(self, item)
fragment.add_css_url(url)
for item in js_files:
item = "static/" + item
url = self.runtime.local_resource_url(self, item)
fragment.add_javascript_url(url)
if js_init: # pragma: no cover
fragment.initialize_js(js_init)
return fragment

# pylint: enable=too-many-arguments
Loading
Loading