Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SE-3220] Add support for multiple statements in the XBlock #12

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 4 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Setup the XBlock
"""
from setuptools import setup
from setuptools import setup, find_packages
import os


Expand All @@ -14,6 +14,7 @@ def package_data(pkg, roots):
"""
stvstnfrd marked this conversation as resolved.
Show resolved Hide resolved
data = []
for root in roots:
data.append(root)
for dirname, _, files in os.walk(os.path.join(pkg, root)):
for fname in files:
data.append(os.path.relpath(os.path.join(dirname, fname), pkg))
Expand Down Expand Up @@ -47,12 +48,10 @@ def is_requirement(line):

setup(
name='xblock-sql-grader',
version='0.1',
version='0.2',
description='SQL Grader XBlock', # TODO: write a better description.
license='AGPLv3',
packages=[
'sql_grader',
],
packages=find_packages(exclude=('sql_grader.tests')),
install_requires=load_requirements('requirements/base.in'),
entry_points={
'xblock.v1': [
Expand Down
22 changes: 20 additions & 2 deletions sql_grader/mixins/grading.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@
log = logging.getLogger('sql_grader')


# pylint: disable=unused-argument
def attempt_safe(dataset, answer_query, verify_query, is_ordered, query):
# pylint: disable=too-many-arguments
def attempt_safe(dataset, answer_query, verify_query, modification_query,
is_ordered, query):
"""
Attempt a SqlProblem, using codejail to sandbox the execution.
"""
results = {
'answer_query': answer_query,
'dataset': dataset,
'verify_query': verify_query,
'modification_query': modification_query,
'is_ordered': is_ordered,
'query': query
}
Expand All @@ -44,6 +46,7 @@ def attempt_safe(dataset, answer_query, verify_query, is_ordered, query):
answer_query=answer_query,
dataset=dataset,
verify_query=verify_query,
modification_query=modification_query,
is_ordered=is_ordered
).attempt(query)

Expand Down Expand Up @@ -145,6 +148,7 @@ def _calculate_score(self):
self.dataset,
self.answer_query,
self.verify_query,
self.modification_query,
self.is_ordered,
self.raw_response
)
Expand Down Expand Up @@ -188,6 +192,18 @@ class XBlockDataMixin:
help=_(
'A secondary verification SQL query, to be used if the '
'answer_query modifies the database (UPDATE, INSERT, DELETE, etc.)'
'. Only enter a single SELECT query here, which is used for '
'matching the answer'
),
default='',
scope=Scope.content,
multiline_editor=True,
)
modification_query = String(
display_name=_('Modification query statements'),
help=_(
'Optional SQL statements, to be executed after the '
'user-submitted query, but before the verify_query.'
),
default='',
scope=Scope.content,
Expand All @@ -204,6 +220,7 @@ class XBlockDataMixin:
'dataset',
'display_name',
'verify_query',
'modification_query',
'is_ordered',
'prompt',
'weight',
Expand Down Expand Up @@ -240,5 +257,6 @@ def provide_context(self, context): # pragma: no cover
'error_class': error_class,
'raw_response': self.raw_response,
'verify_query': self.verify_query,
'modification_query': self.modification_query,
})
return context
61 changes: 51 additions & 10 deletions sql_grader/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ class SqlProblem:
dataset = None
answer_query = None
verify_query = None
modification_query = None
answer_result = None
answer_error = None
is_ordered = True

# pylint: disable=too-many-arguments
Expand All @@ -25,6 +27,7 @@ def __init__(
answer_query=None,
dataset=None,
verify_query=None,
modification_query=None,
is_ordered=True,
):
"""
Expand All @@ -36,20 +39,30 @@ def __init__(
self.is_ordered = is_ordered
self.answer_query = answer_query
self.verify_query = verify_query
self.answer_result, _ = SqlProblem.run_query(
self.modification_query = modification_query
self.answer_result, self.answer_error = SqlProblem.run_query(
stvstnfrd marked this conversation as resolved.
Show resolved Hide resolved
self.database,
answer_query,
verify_query,
modification_query,
)

def attempt(self, query):
"""
Attempt to answer the problem with the provided query
"""
if self.answer_error:
stvstnfrd marked this conversation as resolved.
Show resolved Hide resolved
return (
None,
None,
'Problem setup incorrectly: {}'.format(self.answer_error),
False
)
submission_result, error = SqlProblem.run_query(
self.database,
query,
self.verify_query,
self.modification_query,
)
comparison = SqlProblem.compare_rows(
self.answer_result,
Expand Down Expand Up @@ -93,26 +106,54 @@ def clone_database(source):
return destination

@classmethod
def run_query(cls, source, query, verify_query=None):
def run_query(cls, source, query, verify_query=None,
modification_query=None):
"""
Execute the provided SQL query against a copy of the database
stvstnfrd marked this conversation as resolved.
Show resolved Hide resolved
Execute the provided SQL queries against a copy of the database.
"""
def run(database, query):
def run(database, query, is_single_query):
result = []
message = None
with database as connection:
try:
for row in connection.execute(query):
if is_single_query:
executor_func = connection.execute
else:
executor_func = connection.executescript
rows = executor_func(query)
mavidser marked this conversation as resolved.
Show resolved Hide resolved
# connection.executescript doesn't return anything, so the
# following loop would be a no-op in such cases.
for row in rows:
result.append(row)
except Exception as error: # pylint: disable=broad-except
result = None
message = str(error)
return result, message

database = cls.clone_database(source)
result, error = run(database, query)

# SELECT statements must be run as a single query to be able to
# collect the results. When there's no verify_query in the problem,
# we assume the SELECT statement must be in `query` and therefore
# `query` must be run as a single query.
is_single_query = not verify_query
result, error = run(database, query, is_single_query=is_single_query)
if error:
return None, error

if verify_query:
result, _ = run(database, verify_query)
return result, error
if modification_query:
_, error = run(database, modification_query,
is_single_query=False)
if error:
return None, 'modification_query: {}'.format(error)

result, error = run(database, verify_query,
is_single_query=True)
if error:
return None, 'verify_query: {}'.format(error)

return result, None

@staticmethod
def compare_rows(expected, actual, is_ordered=True):
Expand All @@ -124,8 +165,8 @@ def compare_rows(expected, actual, is_ordered=True):
if len(expected) != len(actual):
return False
if not is_ordered:
expected = sorted(expected)
actual = sorted(actual)
expected = sorted(expected, key=str)
actual = sorted(actual, key=str)
comparison = all(
row_expected == row_actual
for row_expected, row_actual in zip(expected, actual)
Expand Down
5 changes: 5 additions & 0 deletions sql_grader/templates/view.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ <h3 class="problem-header">{{display_name}}</h3>
{% if verify_query %}
<div class="sql_verify_query">
{% trans 'To check your data modification statement, we ran the following query after your modification:' %}
{% if modification_query %}
<pre>
{{ modification_query }}
</pre>
{% endif %}
<pre>
{{ verify_query }}
</pre>
Expand Down