feat!: Removing sandbox folder from platform and installing it from p… (#30402)
* feat!: common/lib/sandbox-packages folder moved to a new library.
This commit is contained in:
26
common/djangoapps/util/tests/test_codejail_includes.py
Normal file
26
common/djangoapps/util/tests/test_codejail_includes.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Tests for codejail-includes package.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
import eia
|
||||
import loncapa
|
||||
from verifiers import draganddrop
|
||||
|
||||
|
||||
class TestCodeJailIncludes(unittest.TestCase):
|
||||
""" tests for codejail includes"""
|
||||
|
||||
def test_loncapa(self):
|
||||
random_integer = loncapa.lc_random(2, 60, 5)
|
||||
assert random_integer <= 60
|
||||
assert random_integer >= 2
|
||||
|
||||
def test_nested_list_and_list1(self):
|
||||
assert draganddrop.PositionsCompare([[1, 2], 40]) == draganddrop.PositionsCompare([1, 3])
|
||||
|
||||
def test_Eia(self):
|
||||
# Test cases. All of these should return True
|
||||
assert eia.iseia(100) # 100 ohm resistor is EIA
|
||||
assert not eia.iseia(101) # 101 is not
|
||||
@@ -15,18 +15,12 @@ CodeJail`__, with a few customized tweaks.
|
||||
__ https://github.com/edx/codejail/blob/master/README.rst
|
||||
|
||||
|
||||
1. At the instruction to install packages into the sandboxed code, you'll
|
||||
1. At the instruction to install packages into the sandboxed code, you'll
|
||||
need to install the requirements from requirements/edx-sandbox::
|
||||
|
||||
$ pip install -r requirements/edx-sandbox/base.txt
|
||||
|
||||
2. At the instruction to create the AppArmor profile, you'll need a line in
|
||||
the profile for the sandbox packages. <EDXPLATFORM> is the full path to
|
||||
your edx_platform repo::
|
||||
|
||||
<EDXPLATFORM>/common/lib/sandbox-packages/** r,
|
||||
|
||||
3. You can configure resource limits in settings.py. A CODE_JAIL setting is
|
||||
2. You can configure resource limits in settings.py. A CODE_JAIL setting is
|
||||
available, a dictionary. The "limits" key lets you adjust the limits for
|
||||
CPU time, real time, and memory use. Setting any of them to zero disables
|
||||
that limit::
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
This directory is in the Python path for sandboxed Python execution.
|
||||
@@ -1,112 +0,0 @@
|
||||
"""
|
||||
Standard resistor values.
|
||||
|
||||
Commonly used for verifying electronic components in circuit classes are
|
||||
standard values, or conversely, for generating realistic component
|
||||
values in parameterized problems. For details, see:
|
||||
|
||||
http://en.wikipedia.org/wiki/Electronic_color_code
|
||||
"""
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
# r is standard name for a resistor. We would like to use it as such.
|
||||
|
||||
import math
|
||||
import numbers
|
||||
|
||||
E6 = [10, 15, 22, 33, 47, 68]
|
||||
|
||||
E12 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82]
|
||||
E24 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82, 11, 13, 16, 20,
|
||||
24, 30, 36, 43, 51, 62, 75, 91]
|
||||
|
||||
E48 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 105,
|
||||
127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 110, 133,
|
||||
162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 115, 140, 169,
|
||||
205, 249, 301, 365, 442, 536, 649, 787, 953]
|
||||
|
||||
E96 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 102,
|
||||
124, 150, 182, 221, 267, 324, 392, 475, 576, 698, 845, 105, 127,
|
||||
154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 107, 130, 158,
|
||||
191, 232, 280, 340, 412, 499, 604, 732, 887, 110, 133, 162, 196,
|
||||
237, 287, 348, 422, 511, 619, 750, 909, 113, 137, 165, 200, 243,
|
||||
294, 357, 432, 523, 634, 768, 931, 115, 140, 169, 205, 249, 301,
|
||||
365, 442, 536, 649, 787, 953, 118, 143, 174, 210, 255, 309, 374,
|
||||
453, 549, 665, 806, 976]
|
||||
|
||||
E192 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 101,
|
||||
123, 149, 180, 218, 264, 320, 388, 470, 569, 690, 835, 102, 124,
|
||||
150, 182, 221, 267, 324, 392, 475, 576, 698, 845, 104, 126, 152,
|
||||
184, 223, 271, 328, 397, 481, 583, 706, 856, 105, 127, 154, 187,
|
||||
226, 274, 332, 402, 487, 590, 715, 866, 106, 129, 156, 189, 229,
|
||||
277, 336, 407, 493, 597, 723, 876, 107, 130, 158, 191, 232, 280,
|
||||
340, 412, 499, 604, 732, 887, 109, 132, 160, 193, 234, 284, 344,
|
||||
417, 505, 612, 741, 898, 110, 133, 162, 196, 237, 287, 348, 422,
|
||||
511, 619, 750, 909, 111, 135, 164, 198, 240, 291, 352, 427, 517,
|
||||
626, 759, 920, 113, 137, 165, 200, 243, 294, 357, 432, 523, 634,
|
||||
768, 931, 114, 138, 167, 203, 246, 298, 361, 437, 530, 642, 777,
|
||||
942, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953,
|
||||
117, 142, 172, 208, 252, 305, 370, 448, 542, 657, 796, 965, 118,
|
||||
143, 174, 210, 255, 309, 374, 453, 549, 665, 806, 976, 120, 145,
|
||||
176, 213, 258, 312, 379, 459, 556, 673, 816, 988]
|
||||
|
||||
|
||||
def iseia(r, valid_types=(E6, E12, E24)):
|
||||
'''
|
||||
Check if a component is a valid EIA value.
|
||||
|
||||
By default, check 5% component values
|
||||
'''
|
||||
|
||||
# Step 1: Discount things which are not numbers
|
||||
if not isinstance(r, numbers.Number) or \
|
||||
r < 0 or \
|
||||
math.isnan(r) or \
|
||||
math.isinf(r):
|
||||
return False
|
||||
|
||||
# Special case: 0 is an okay resistor
|
||||
if r == 0:
|
||||
return True
|
||||
|
||||
# Step 2: Move into the range [100, 1000)
|
||||
while r < 100:
|
||||
r = r * 10
|
||||
while r >= 1000:
|
||||
r = r / 10
|
||||
|
||||
# Step 3: Discount things which are not integers, and cast to int
|
||||
if abs(r - round(r)) > 0.01:
|
||||
return False
|
||||
r = int(round(r))
|
||||
|
||||
# Step 4: Check if we're a valid EIA value
|
||||
for type_list in valid_types:
|
||||
if r in type_list:
|
||||
return True
|
||||
if int(r / 10.) in type_list and (r % 10) == 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Test cases. All of these should return True
|
||||
print(iseia(100)) # 100 ohm resistor is EIA
|
||||
print(not iseia(101)) # 101 is not
|
||||
print(not iseia(100.3)) # Floating point close to EIA is not EIA
|
||||
print(iseia(100.001)) # But within floating point error is
|
||||
print(iseia(1e5)) # We handle big numbers well
|
||||
print(iseia(2200)) # We handle middle-of-the-list well
|
||||
# We can handle 1% components correctly; 2.2k is EIA24, but not EIA48.
|
||||
print(not iseia(2200, (E48, E96, E192)))
|
||||
print(iseia(5490e2, (E48, E96, E192)))
|
||||
print(iseia(2200))
|
||||
print(not iseia(5490e2))
|
||||
print(iseia(1e-5)) # We handle little numbers well
|
||||
print(not iseia("Hello")) # Junk handled okay
|
||||
print(not iseia(float('NaN')))
|
||||
print(not iseia(-1))
|
||||
print(not iseia(iseia))
|
||||
print(not iseia(float('Inf')))
|
||||
print(iseia(0)) # Corner case. 0 is a standard resistor value.
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/usr/bin/python # lint-amnesty, pylint: disable=missing-module-docstring
|
||||
|
||||
from .loncapa_check import * # lint-amnesty, pylint: disable=redefined-builtin
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/python # lint-amnesty, pylint: disable=missing-module-docstring
|
||||
#
|
||||
# File: mitx/lib/loncapa/loncapa_check.py
|
||||
#
|
||||
# Python functions which duplicate the standard comparison functions available to LON-CAPA problems.
|
||||
# Used in translating LON-CAPA problems to i4x problem specification language.
|
||||
|
||||
|
||||
import math
|
||||
import random
|
||||
from six.moves import range
|
||||
|
||||
|
||||
def lc_random(lower, upper, stepsize):
|
||||
'''
|
||||
like random.randrange but lower and upper can be non-integer
|
||||
'''
|
||||
nstep = int((upper - lower) / (1.0 * stepsize))
|
||||
choices = [lower + x * stepsize for x in range(nstep)]
|
||||
return random.choice(choices)
|
||||
|
||||
|
||||
def lc_choose(index, *args):
|
||||
'''
|
||||
return args[index]
|
||||
'''
|
||||
try:
|
||||
return args[int(index) - 1]
|
||||
except Exception as err: # lint-amnesty, pylint: disable=broad-except, unused-variable
|
||||
pass
|
||||
if len(args): # lint-amnesty, pylint: disable=len-as-condition
|
||||
return args[0]
|
||||
raise Exception(
|
||||
"loncapa_check.lc_choose error, index={index}, args={args}".format(
|
||||
index=index,
|
||||
args=args,
|
||||
)
|
||||
)
|
||||
|
||||
deg2rad = math.pi / 180.0
|
||||
rad2deg = 180.0 / math.pi
|
||||
@@ -1,17 +0,0 @@
|
||||
# lint-amnesty, pylint: disable=missing-module-docstring
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="sandbox-packages",
|
||||
version="0.1.1",
|
||||
packages=[
|
||||
"loncapa",
|
||||
"verifiers",
|
||||
],
|
||||
py_modules=[
|
||||
"eia",
|
||||
],
|
||||
install_requires=[
|
||||
],
|
||||
)
|
||||
@@ -1,433 +0,0 @@
|
||||
""" Grader of drag and drop input.
|
||||
|
||||
Client side behavior: user can drag and drop images from list on base image.
|
||||
|
||||
|
||||
Then json returned from client is:
|
||||
{
|
||||
"draggable": [
|
||||
{ "image1": "t1" },
|
||||
{ "ant": "t2" },
|
||||
{ "molecule": "t3" },
|
||||
]
|
||||
}
|
||||
values are target names.
|
||||
|
||||
or:
|
||||
{
|
||||
"draggable": [
|
||||
{ "image1": "[10, 20]" },
|
||||
{ "ant": "[30, 40]" },
|
||||
{ "molecule": "[100, 200]" },
|
||||
]
|
||||
}
|
||||
values are (x, y) coordinates of centers of dragged images.
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
import six
|
||||
from six.moves import zip
|
||||
|
||||
|
||||
def flat_user_answer(user_answer):
|
||||
"""
|
||||
Convert nested `user_answer` to flat format.
|
||||
|
||||
{'up': {'first': {'p': 'p_l'}}}
|
||||
|
||||
to
|
||||
|
||||
{'up': 'p_l[p][first]'}
|
||||
"""
|
||||
|
||||
def parse_user_answer(answer):
|
||||
key = list(answer.keys())[0]
|
||||
value = list(answer.values())[0]
|
||||
if isinstance(value, dict):
|
||||
|
||||
# Make complex value:
|
||||
# Example:
|
||||
# Create like 'p_l[p][first]' from {'first': {'p': 'p_l'}
|
||||
complex_value_list = []
|
||||
v_value = value
|
||||
while isinstance(v_value, dict):
|
||||
v_key = list(v_value.keys())[0]
|
||||
v_value = list(v_value.values())[0]
|
||||
complex_value_list.append(v_key)
|
||||
|
||||
complex_value = '{0}'.format(v_value)
|
||||
for i in reversed(complex_value_list):
|
||||
complex_value = '{0}[{1}]'.format(complex_value, i)
|
||||
|
||||
res = {key: complex_value}
|
||||
return res
|
||||
else:
|
||||
return answer
|
||||
|
||||
result = []
|
||||
for answer in user_answer:
|
||||
parse_answer = parse_user_answer(answer)
|
||||
result.append(parse_answer)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class PositionsCompare(list):
|
||||
""" Class for comparing positions.
|
||||
|
||||
Args:
|
||||
list or string::
|
||||
"abc" - target
|
||||
[10, 20] - list of integers
|
||||
[[10, 20], 200] list of list and integer
|
||||
|
||||
"""
|
||||
def __eq__(self, other):
|
||||
""" Compares two arguments.
|
||||
|
||||
Default lists behavior is conversion of string "abc" to list
|
||||
["a", "b", "c"]. We will use that.
|
||||
|
||||
If self or other is empty - returns False.
|
||||
|
||||
Args:
|
||||
self, other: str, unicode, list, int, float
|
||||
|
||||
Returns: bool
|
||||
"""
|
||||
# checks if self or other is not empty list (empty lists = false)
|
||||
if not self or not other:
|
||||
return False
|
||||
|
||||
if (isinstance(self[0], (list, int, float)) and
|
||||
isinstance(other[0], (list, int, float))):
|
||||
return self.coordinate_positions_compare(other)
|
||||
|
||||
elif (isinstance(self[0], (six.text_type, str)) and
|
||||
isinstance(other[0], (six.text_type, str))):
|
||||
return ''.join(self) == ''.join(other)
|
||||
else: # improper argument types: no (float / int or lists of list
|
||||
#and float / int pair) or two string / unicode lists pair
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def coordinate_positions_compare(self, other, r=10):
|
||||
""" Checks if self is equal to other inside radius of forgiveness
|
||||
(default 10 px).
|
||||
|
||||
Args:
|
||||
self, other: [x, y] or [[x, y], r], where r is radius of
|
||||
forgiveness;
|
||||
x, y, r: int
|
||||
|
||||
Returns: bool.
|
||||
"""
|
||||
# get max radius of forgiveness
|
||||
if isinstance(self[0], list): # [(x, y), r] case
|
||||
r = max(self[1], r)
|
||||
x1, y1 = self[0]
|
||||
else:
|
||||
x1, y1 = self
|
||||
|
||||
if isinstance(other[0], list): # [(x, y), r] case
|
||||
r = max(other[1], r)
|
||||
x2, y2 = other[0]
|
||||
else:
|
||||
x2, y2 = other
|
||||
|
||||
if (x2 - x1) ** 2 + (y2 - y1) ** 2 > r * r:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DragAndDrop(object):
|
||||
""" Grader class for drag and drop inputtype.
|
||||
"""
|
||||
|
||||
def grade(self):
|
||||
''' Grader user answer.
|
||||
|
||||
Checks if every draggable isplaced on proper target or on proper
|
||||
coordinates within radius of forgiveness (default is 10).
|
||||
|
||||
Returns: bool.
|
||||
'''
|
||||
for draggable in self.excess_draggables:
|
||||
if self.excess_draggables[draggable]:
|
||||
return False # user answer has more draggables than correct answer
|
||||
|
||||
# Number of draggables in user_groups may be differ that in
|
||||
# correct_groups, that is incorrect, except special case with 'number'
|
||||
for index, draggable_ids in enumerate(self.correct_groups):
|
||||
# 'number' rule special case
|
||||
# for reusable draggables we may get in self.user_groups
|
||||
# {'1': [u'2', u'2', u'2'], '0': [u'1', u'1'], '2': [u'3']}
|
||||
# if '+number' is in rule - do not remove duplicates and strip
|
||||
# '+number' from rule
|
||||
current_rule = list(self.correct_positions[index].keys())[0]
|
||||
if 'number' in current_rule:
|
||||
rule_values = self.correct_positions[index][current_rule]
|
||||
# clean rule, do not do clean duplicate items
|
||||
self.correct_positions[index].pop(current_rule, None)
|
||||
parsed_rule = current_rule.replace('+', '').replace('number', '')
|
||||
self.correct_positions[index][parsed_rule] = rule_values
|
||||
else: # remove dublicates
|
||||
self.user_groups[index] = list(set(self.user_groups[index]))
|
||||
|
||||
if sorted(draggable_ids) != sorted(self.user_groups[index]):
|
||||
return False
|
||||
|
||||
# Check that in every group, for rule of that group, user positions of
|
||||
# every element are equal with correct positions
|
||||
for index, _ in enumerate(self.correct_groups):
|
||||
rules_executed = 0
|
||||
for rule in ('exact', 'anyof', 'unordered_equal'):
|
||||
# every group has only one rule
|
||||
if self.correct_positions[index].get(rule, None):
|
||||
rules_executed += 1
|
||||
if not self.compare_positions(
|
||||
self.correct_positions[index][rule],
|
||||
self.user_positions[index]['user'], flag=rule):
|
||||
return False
|
||||
if not rules_executed: # no correct rules for current group
|
||||
# probably xml content mistake - wrong rules names
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def compare_positions(self, correct, user, flag):
|
||||
""" Compares two lists of positions with flag rules. Order of
|
||||
correct/user arguments is matter only in 'anyof' flag.
|
||||
|
||||
Rules description:
|
||||
|
||||
'exact' means 1-1 ordered relationship::
|
||||
|
||||
[el1, el2, el3] is 'exact' equal to [el5, el6, el7] when
|
||||
el1 == el5, el2 == el6, el3 == el7.
|
||||
Equality function is custom, see below.
|
||||
|
||||
|
||||
'anyof' means subset relationship::
|
||||
|
||||
user = [el1, el2] is 'anyof' equal to correct = [el1, el2, el3]
|
||||
when
|
||||
set(user) <= set(correct).
|
||||
|
||||
'anyof' is ordered relationship. It always checks if user
|
||||
is subset of correct
|
||||
|
||||
Equality function is custom, see below.
|
||||
|
||||
Examples:
|
||||
|
||||
- many draggables per position:
|
||||
user ['1', '2', '2', '2'] is 'anyof' equal to ['1', '2', '3']
|
||||
|
||||
- draggables can be placed in any order:
|
||||
user ['1', '2', '3', '4'] is 'anyof' equal to ['4', '2', '1', 3']
|
||||
|
||||
'unordered_equal' is same as 'exact' but disregards on order
|
||||
|
||||
Equality functions:
|
||||
|
||||
Equality functon depends on type of element. They declared in
|
||||
PositionsCompare class. For position like targets
|
||||
ids ("t1", "t2", etc..) it is string equality function. For coordinate
|
||||
positions ([1, 2] or [[1, 2], 15]) it is coordinate_positions_compare
|
||||
function (see docstrings in PositionsCompare class)
|
||||
|
||||
Args:
|
||||
correst, user: lists of positions
|
||||
|
||||
Returns: True if within rule lists are equal, otherwise False.
|
||||
"""
|
||||
if flag == 'exact':
|
||||
if len(correct) != len(user):
|
||||
return False
|
||||
for el1, el2 in zip(correct, user):
|
||||
if PositionsCompare(el1) != PositionsCompare(el2):
|
||||
return False
|
||||
|
||||
if flag == 'anyof':
|
||||
for u_el in user:
|
||||
for c_el in correct:
|
||||
if PositionsCompare(u_el) == PositionsCompare(c_el):
|
||||
break
|
||||
else:
|
||||
# General: the else is executed after the for,
|
||||
# only if the for terminates normally (not by a break)
|
||||
|
||||
# In this case, 'for' is terminated normally if every element
|
||||
# from 'correct' list isn't equal to concrete element from
|
||||
# 'user' list. So as we found one element from 'user' list,
|
||||
# that not in 'correct' list - we return False
|
||||
return False
|
||||
|
||||
if flag == 'unordered_equal':
|
||||
if len(correct) != len(user):
|
||||
return False
|
||||
temp = correct[:]
|
||||
for u_el in user:
|
||||
for c_el in temp:
|
||||
if PositionsCompare(u_el) == PositionsCompare(c_el):
|
||||
temp.remove(c_el)
|
||||
break
|
||||
else:
|
||||
# same as upper - if we found element from 'user' list,
|
||||
# that not in 'correct' list - we return False.
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __init__(self, correct_answer, user_answer):
|
||||
""" Populates DragAndDrop variables from user_answer and correct_answer.
|
||||
If correct_answer is dict, converts it to list.
|
||||
Correct answer in dict form is simple structure for fast and simple
|
||||
grading. Example of correct answer dict example::
|
||||
|
||||
correct_answer = {'name4': 't1',
|
||||
'name_with_icon': 't1',
|
||||
'5': 't2',
|
||||
'7': 't2'}
|
||||
|
||||
It is draggable_name: dragable_position mapping.
|
||||
|
||||
Advanced form converted from simple form uses 'exact' rule
|
||||
for matching.
|
||||
|
||||
Correct answer in list form is designed for advanced cases::
|
||||
|
||||
correct_answers = [
|
||||
{
|
||||
'draggables': ['1', '2', '3', '4', '5', '6'],
|
||||
'targets': [
|
||||
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'],
|
||||
'rule': 'anyof'},
|
||||
{
|
||||
'draggables': ['7', '8', '9', '10'],
|
||||
'targets': ['p_left_1', 'p_left_2', 'p_right_1', 'p_right_2'],
|
||||
'rule': 'anyof'
|
||||
}
|
||||
]
|
||||
|
||||
Advanced answer in list form is list of dicts, and every dict must have
|
||||
3 keys: 'draggables', 'targets' and 'rule'. 'Draggables' value is
|
||||
list of draggables ids, 'targes' values are list of targets ids, 'rule'
|
||||
value one of 'exact', 'anyof', 'unordered_equal', 'anyof+number',
|
||||
'unordered_equal+number'
|
||||
|
||||
Advanced form uses "all dicts must match with their rule" logic.
|
||||
|
||||
Same draggable cannot appears more that in one dict.
|
||||
|
||||
Behavior is more widely explained in sphinx documentation.
|
||||
|
||||
Args:
|
||||
user_answer: json
|
||||
correct_answer: dict or list
|
||||
"""
|
||||
|
||||
self.correct_groups = [] # Correct groups from xml.
|
||||
self.correct_positions = [] # Correct positions for comparing.
|
||||
self.user_groups = [] # Will be populated from user answer.
|
||||
self.user_positions = [] # Will be populated from user answer.
|
||||
|
||||
# Convert from dict answer format to list format.
|
||||
if isinstance(correct_answer, dict):
|
||||
tmp = []
|
||||
for key in sorted(correct_answer.keys()):
|
||||
value = correct_answer[key]
|
||||
tmp.append({
|
||||
'draggables': [key],
|
||||
'targets': [value],
|
||||
'rule': 'exact'})
|
||||
correct_answer = tmp
|
||||
|
||||
# Convert string `user_answer` to object.
|
||||
user_answer = json.loads(user_answer)
|
||||
|
||||
# This dictionary will hold a key for each draggable the user placed on
|
||||
# the image. The value is True if that draggable is not mentioned in any
|
||||
# correct_answer entries. If the draggable is mentioned in at least one
|
||||
# correct_answer entry, the value is False.
|
||||
# default to consider every user answer excess until proven otherwise.
|
||||
self.excess_draggables = dict(
|
||||
(list(users_draggable.keys())[0], True)
|
||||
for users_draggable in user_answer
|
||||
)
|
||||
|
||||
# Convert nested `user_answer` to flat format.
|
||||
user_answer = flat_user_answer(user_answer)
|
||||
|
||||
# Create identical data structures from user answer and correct answer.
|
||||
for answer in correct_answer:
|
||||
user_groups_data = []
|
||||
user_positions_data = []
|
||||
for draggable_dict in user_answer:
|
||||
# Draggable_dict is 1-to-1 {draggable_name: position}.
|
||||
draggable_name = list(draggable_dict.keys())[0]
|
||||
if draggable_name in answer['draggables']:
|
||||
user_groups_data.append(draggable_name)
|
||||
user_positions_data.append(
|
||||
draggable_dict[draggable_name]
|
||||
)
|
||||
# proved that this is not excess
|
||||
self.excess_draggables[draggable_name] = False
|
||||
|
||||
self.correct_groups.append(answer['draggables'])
|
||||
self.correct_positions.append({answer['rule']: answer['targets']})
|
||||
self.user_groups.append(user_groups_data)
|
||||
self.user_positions.append({'user': user_positions_data})
|
||||
|
||||
|
||||
def grade(user_input, correct_answer):
|
||||
""" Creates DragAndDrop instance from user_input and correct_answer and
|
||||
calls DragAndDrop.grade for grading.
|
||||
|
||||
Supports two interfaces for correct_answer: dict and list.
|
||||
|
||||
Args:
|
||||
user_input: json. Format::
|
||||
|
||||
{ "draggables":
|
||||
[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]}'
|
||||
|
||||
or
|
||||
|
||||
{"draggables": [{"1": "t1"}, \
|
||||
{"name_with_icon": "t2"}]}
|
||||
|
||||
correct_answer: dict or list.
|
||||
|
||||
Dict form::
|
||||
|
||||
{'1': 't1', 'name_with_icon': 't2'}
|
||||
|
||||
or
|
||||
|
||||
{'1': '[10, 10]', 'name_with_icon': '[[10, 10], 20]'}
|
||||
|
||||
List form::
|
||||
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['l3_o', 'l10_o'],
|
||||
'targets': ['t1_o', 't9_o'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['l1_c', 'l8_c'],
|
||||
'targets': ['t5_c', 't6_c'],
|
||||
'rule': 'anyof'
|
||||
}
|
||||
]
|
||||
|
||||
Returns: bool
|
||||
"""
|
||||
return DragAndDrop(correct_answer=correct_answer,
|
||||
user_answer=user_input).grade()
|
||||
@@ -1,843 +0,0 @@
|
||||
# lint-amnesty, pylint: disable=missing-module-docstring
|
||||
|
||||
import json
|
||||
import unittest
|
||||
|
||||
from . import draganddrop
|
||||
|
||||
from .draganddrop import PositionsCompare
|
||||
|
||||
|
||||
class Test_PositionsCompare(unittest.TestCase):
|
||||
""" describe"""
|
||||
|
||||
def test_nested_list_and_list1(self):
|
||||
assert PositionsCompare([[1, 2], 40]) == PositionsCompare([1, 3])
|
||||
|
||||
def test_nested_list_and_list2(self):
|
||||
assert PositionsCompare([1, 12]) != PositionsCompare([1, 1])
|
||||
|
||||
def test_list_and_list1(self):
|
||||
assert PositionsCompare([[1, 2], 12]) != PositionsCompare([1, 15])
|
||||
|
||||
def test_list_and_list2(self):
|
||||
assert PositionsCompare([1, 11]) == PositionsCompare([1, 1])
|
||||
|
||||
def test_numerical_list_and_string_list(self):
|
||||
assert PositionsCompare([1, 2]) != PositionsCompare(['1'])
|
||||
|
||||
def test_string_and_string_list1(self):
|
||||
assert PositionsCompare('1') == PositionsCompare(['1'])
|
||||
|
||||
def test_string_and_string_list2(self):
|
||||
assert PositionsCompare('abc') == PositionsCompare('abc')
|
||||
|
||||
def test_string_and_string_list3(self):
|
||||
assert PositionsCompare('abd') != PositionsCompare('abe')
|
||||
|
||||
def test_float_and_string(self):
|
||||
assert PositionsCompare([3.5, 5.7]) != PositionsCompare(['1'])
|
||||
|
||||
def test_floats_and_ints(self):
|
||||
assert PositionsCompare([3.5, 4.5]) == PositionsCompare([5, 7])
|
||||
|
||||
|
||||
class Test_DragAndDrop_Grade(unittest.TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
|
||||
def test_targets_are_draggable_1(self):
|
||||
user_input = json.dumps([
|
||||
{'p': 'p_l'},
|
||||
{'up': {'first': {'p': 'p_l'}}}
|
||||
])
|
||||
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['p'],
|
||||
'targets': ['p_l', 'p_r'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['up'],
|
||||
'targets': [
|
||||
'p_l[p][first]'
|
||||
],
|
||||
'rule': 'anyof'
|
||||
}
|
||||
]
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_targets_are_draggable_2(self):
|
||||
user_input = json.dumps([
|
||||
{'p': 'p_l'},
|
||||
{'p': 'p_r'},
|
||||
{'s': 's_l'},
|
||||
{'s': 's_r'},
|
||||
{'up': {'1': {'p': 'p_l'}}},
|
||||
{'up': {'3': {'p': 'p_l'}}},
|
||||
{'up': {'1': {'p': 'p_r'}}},
|
||||
{'up': {'3': {'p': 'p_r'}}},
|
||||
{'up_and_down': {'1': {'s': 's_l'}}},
|
||||
{'up_and_down': {'1': {'s': 's_r'}}}
|
||||
])
|
||||
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['p'],
|
||||
'targets': ['p_l', 'p_r'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['s'],
|
||||
'targets': ['s_l', 's_r'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up_and_down'],
|
||||
'targets': ['s_l[s][1]', 's_r[s][1]'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up'],
|
||||
'targets': [
|
||||
'p_l[p][1]',
|
||||
'p_l[p][3]',
|
||||
'p_r[p][1]',
|
||||
'p_r[p][3]',
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
}
|
||||
]
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_targets_are_draggable_2_manual_parsing(self):
|
||||
user_input = json.dumps([
|
||||
{'up': 'p_l[p][1]'},
|
||||
{'p': 'p_l'},
|
||||
{'up': 'p_l[p][3]'},
|
||||
{'up': 'p_r[p][1]'},
|
||||
{'p': 'p_r'},
|
||||
{'up': 'p_r[p][3]'},
|
||||
{'up_and_down': 's_l[s][1]'},
|
||||
{'s': 's_l'},
|
||||
{'up_and_down': 's_r[s][1]'},
|
||||
{'s': 's_r'}
|
||||
])
|
||||
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['p'],
|
||||
'targets': ['p_l', 'p_r'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['s'],
|
||||
'targets': ['s_l', 's_r'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up_and_down'],
|
||||
'targets': ['s_l[s][1]', 's_r[s][1]'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up'],
|
||||
'targets': [
|
||||
'p_l[p][1]',
|
||||
'p_l[p][3]',
|
||||
'p_r[p][1]',
|
||||
'p_r[p][3]',
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
}
|
||||
]
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_targets_are_draggable_3_nested(self):
|
||||
user_input = json.dumps([
|
||||
{'molecule': 'left_side_tagret'},
|
||||
{'molecule': 'right_side_tagret'},
|
||||
{'p': {'p_target': {'molecule': 'left_side_tagret'}}},
|
||||
{'p': {'p_target': {'molecule': 'right_side_tagret'}}},
|
||||
{'s': {'s_target': {'molecule': 'left_side_tagret'}}},
|
||||
{'s': {'s_target': {'molecule': 'right_side_tagret'}}},
|
||||
{'up': {'1': {'p': {'p_target': {'molecule': 'left_side_tagret'}}}}},
|
||||
{'up': {'3': {'p': {'p_target': {'molecule': 'left_side_tagret'}}}}},
|
||||
{'up': {'1': {'p': {'p_target': {'molecule': 'right_side_tagret'}}}}},
|
||||
{'up': {'3': {'p': {'p_target': {'molecule': 'right_side_tagret'}}}}},
|
||||
{'up_and_down': {'1': {'s': {'s_target': {'molecule': 'left_side_tagret'}}}}},
|
||||
{'up_and_down': {'1': {'s': {'s_target': {'molecule': 'right_side_tagret'}}}}}
|
||||
])
|
||||
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['molecule'],
|
||||
'targets': ['left_side_tagret', 'right_side_tagret'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['p'],
|
||||
'targets': [
|
||||
'left_side_tagret[molecule][p_target]',
|
||||
'right_side_tagret[molecule][p_target]',
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['s'],
|
||||
'targets': [
|
||||
'left_side_tagret[molecule][s_target]',
|
||||
'right_side_tagret[molecule][s_target]',
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up_and_down'],
|
||||
'targets': [
|
||||
'left_side_tagret[molecule][s_target][s][1]',
|
||||
'right_side_tagret[molecule][s_target][s][1]',
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up'],
|
||||
'targets': [
|
||||
'left_side_tagret[molecule][p_target][p][1]',
|
||||
'left_side_tagret[molecule][p_target][p][3]',
|
||||
'right_side_tagret[molecule][p_target][p][1]',
|
||||
'right_side_tagret[molecule][p_target][p][3]',
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
}
|
||||
]
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_targets_are_draggable_4_real_example(self):
|
||||
user_input = json.dumps([
|
||||
{'single_draggable': 's_l'},
|
||||
{'single_draggable': 's_r'},
|
||||
{'single_draggable': 'p_sigma'},
|
||||
{'single_draggable': 'p_sigma*'},
|
||||
{'single_draggable': 's_sigma'},
|
||||
{'single_draggable': 's_sigma*'},
|
||||
{'double_draggable': 'p_pi*'},
|
||||
{'double_draggable': 'p_pi'},
|
||||
{'triple_draggable': 'p_l'},
|
||||
{'triple_draggable': 'p_r'},
|
||||
{'up': {'1': {'triple_draggable': 'p_l'}}},
|
||||
{'up': {'2': {'triple_draggable': 'p_l'}}},
|
||||
{'up': {'2': {'triple_draggable': 'p_r'}}},
|
||||
{'up': {'3': {'triple_draggable': 'p_r'}}},
|
||||
{'up_and_down': {'1': {'single_draggable': 's_l'}}},
|
||||
{'up_and_down': {'1': {'single_draggable': 's_r'}}},
|
||||
{'up_and_down': {'1': {'single_draggable': 's_sigma'}}},
|
||||
{'up_and_down': {'1': {'single_draggable': 's_sigma*'}}},
|
||||
{'up_and_down': {'1': {'double_draggable': 'p_pi'}}},
|
||||
{'up_and_down': {'2': {'double_draggable': 'p_pi'}}}
|
||||
])
|
||||
|
||||
# 10 targets:
|
||||
# s_l, s_r, p_l, p_r, s_sigma, s_sigma*, p_pi, p_sigma, p_pi*, p_sigma*
|
||||
#
|
||||
# 3 draggable objects, which have targets (internal target ids - 1, 2, 3):
|
||||
# single_draggable, double_draggable, triple_draggable
|
||||
#
|
||||
# 2 draggable objects:
|
||||
# up, up_and_down
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['triple_draggable'],
|
||||
'targets': ['p_l', 'p_r'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['double_draggable'],
|
||||
'targets': ['p_pi', 'p_pi*'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['single_draggable'],
|
||||
'targets': ['s_l', 's_r', 's_sigma', 's_sigma*', 'p_sigma', 'p_sigma*'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up'],
|
||||
'targets': [
|
||||
'p_l[triple_draggable][1]',
|
||||
'p_l[triple_draggable][2]',
|
||||
'p_r[triple_draggable][2]',
|
||||
'p_r[triple_draggable][3]',
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up_and_down'],
|
||||
'targets': [
|
||||
's_l[single_draggable][1]',
|
||||
's_r[single_draggable][1]',
|
||||
's_sigma[single_draggable][1]',
|
||||
's_sigma*[single_draggable][1]',
|
||||
'p_pi[double_draggable][1]',
|
||||
'p_pi[double_draggable][2]',
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
|
||||
]
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_targets_true(self):
|
||||
user_input = '[{"1": "t1"}, \
|
||||
{"name_with_icon": "t2"}]'
|
||||
correct_answer = {'1': 't1', 'name_with_icon': 't2'}
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_expect_no_actions_wrong(self):
|
||||
user_input = '[{"1": "t1"}, \
|
||||
{"name_with_icon": "t2"}]'
|
||||
correct_answer = []
|
||||
assert not draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_expect_no_actions_right(self):
|
||||
user_input = '[]'
|
||||
correct_answer = []
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_targets_false(self):
|
||||
user_input = '[{"1": "t1"}, \
|
||||
{"name_with_icon": "t2"}]'
|
||||
correct_answer = {'1': 't3', 'name_with_icon': 't2'}
|
||||
assert not draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_multiple_images_per_target_true(self):
|
||||
user_input = '[{"1": "t1"}, {"name_with_icon": "t2"}, \
|
||||
{"2": "t1"}]'
|
||||
correct_answer = {'1': 't1', 'name_with_icon': 't2', '2': 't1'}
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_multiple_images_per_target_false(self):
|
||||
user_input = '[{"1": "t1"}, {"name_with_icon": "t2"}, \
|
||||
{"2": "t1"}]'
|
||||
correct_answer = {'1': 't2', 'name_with_icon': 't2', '2': 't1'}
|
||||
assert not draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_targets_and_positions(self):
|
||||
user_input = '[{"1": [10,10]}, \
|
||||
{"name_with_icon": [[10,10],4]}]'
|
||||
correct_answer = {'1': [10, 10], 'name_with_icon': [[10, 10], 4]}
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_position_and_targets(self):
|
||||
user_input = '[{"1": "t1"}, {"name_with_icon": "t2"}]'
|
||||
correct_answer = {'1': 't1', 'name_with_icon': 't2'}
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_positions_exact(self):
|
||||
user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]'
|
||||
correct_answer = {'1': [10, 10], 'name_with_icon': [20, 20]}
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_positions_false(self):
|
||||
user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]'
|
||||
correct_answer = {'1': [25, 25], 'name_with_icon': [20, 20]}
|
||||
assert not draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_positions_true_in_radius(self):
|
||||
user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]'
|
||||
correct_answer = {'1': [14, 14], 'name_with_icon': [20, 20]}
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_positions_true_in_manual_radius(self):
|
||||
user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]'
|
||||
correct_answer = {'1': [[40, 10], 30], 'name_with_icon': [20, 20]}
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_positions_false_in_manual_radius(self):
|
||||
user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]'
|
||||
correct_answer = {'1': [[40, 10], 29], 'name_with_icon': [20, 20]}
|
||||
assert not draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_correct_answer_not_has_key_from_user_answer(self):
|
||||
user_input = '[{"1": "t1"}, {"name_with_icon": "t2"}]'
|
||||
correct_answer = {'3': 't3', 'name_with_icon': 't2'}
|
||||
assert not draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_anywhere(self):
|
||||
"""Draggables can be places anywhere on base image.
|
||||
Place grass in the middle of the image and ant in the
|
||||
right upper corner."""
|
||||
user_input = '[{"ant":[610.5,57.449951171875]},\
|
||||
{"grass":[322.5,199.449951171875]}]'
|
||||
|
||||
correct_answer = {'grass': [[300, 200], 200], 'ant': [[500, 0], 200]}
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_lcao_correct(self):
|
||||
"""Describe carbon molecule in LCAO-MO"""
|
||||
user_input = '[{"1":"s_left"}, \
|
||||
{"5":"s_right"},{"4":"s_sigma"},{"6":"s_sigma_star"},{"7":"p_left_1"}, \
|
||||
{"8":"p_left_2"},{"10":"p_right_1"},{"9":"p_right_2"}, \
|
||||
{"2":"p_pi_1"},{"3":"p_pi_2"},{"11":"s_sigma_name"}, \
|
||||
{"13":"s_sigma_star_name"},{"15":"p_pi_name"},{"16":"p_pi_star_name"}, \
|
||||
{"12":"p_sigma_name"},{"14":"p_sigma_star_name"}]'
|
||||
|
||||
correct_answer = [{
|
||||
'draggables': ['1', '2', '3', '4', '5', '6'],
|
||||
'targets': [
|
||||
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'
|
||||
],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['7', '8', '9', '10'],
|
||||
'targets': ['p_left_1', 'p_left_2', 'p_right_1', 'p_right_2'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['11', '12'],
|
||||
'targets': ['s_sigma_name', 'p_sigma_name'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['13', '14'],
|
||||
'targets': ['s_sigma_star_name', 'p_sigma_star_name'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['15'],
|
||||
'targets': ['p_pi_name'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['16'],
|
||||
'targets': ['p_pi_star_name'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_lcao_extra_element_incorrect(self):
|
||||
"""Describe carbon molecule in LCAO-MO"""
|
||||
user_input = '[{"1":"s_left"}, \
|
||||
{"5":"s_right"},{"4":"s_sigma"},{"6":"s_sigma_star"},{"7":"p_left_1"}, \
|
||||
{"8":"p_left_2"},{"17":"p_left_3"},{"10":"p_right_1"},{"9":"p_right_2"}, \
|
||||
{"2":"p_pi_1"},{"3":"p_pi_2"},{"11":"s_sigma_name"}, \
|
||||
{"13":"s_sigma_star_name"},{"15":"p_pi_name"},{"16":"p_pi_star_name"}, \
|
||||
{"12":"p_sigma_name"},{"14":"p_sigma_star_name"}]'
|
||||
|
||||
correct_answer = [{
|
||||
'draggables': ['1', '2', '3', '4', '5', '6'],
|
||||
'targets': [
|
||||
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'
|
||||
],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['7', '8', '9', '10'],
|
||||
'targets': ['p_left_1', 'p_left_2', 'p_right_1', 'p_right_2'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['11', '12'],
|
||||
'targets': ['s_sigma_name', 'p_sigma_name'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['13', '14'],
|
||||
'targets': ['s_sigma_star_name', 'p_sigma_star_name'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['15'],
|
||||
'targets': ['p_pi_name'],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['16'],
|
||||
'targets': ['p_pi_star_name'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
|
||||
assert not draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_reuse_draggable_no_mupliples(self):
|
||||
"""Test reusable draggables (no mupltiple draggables per target)"""
|
||||
user_input = '[{"1":"target1"}, \
|
||||
{"2":"target2"},{"1":"target3"},{"2":"target4"},{"2":"target5"}, \
|
||||
{"3":"target6"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['1'],
|
||||
'targets': ['target1', 'target3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['2'],
|
||||
'targets': ['target2', 'target4', 'target5'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['3'],
|
||||
'targets': ['target6'],
|
||||
'rule': 'anyof'
|
||||
}
|
||||
]
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_reuse_draggable_with_mupliples(self):
|
||||
"""Test reusable draggables with mupltiple draggables per target"""
|
||||
user_input = '[{"1":"target1"}, \
|
||||
{"2":"target2"},{"1":"target1"},{"2":"target4"},{"2":"target4"}, \
|
||||
{"3":"target6"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['1'],
|
||||
'targets': ['target1', 'target3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['2'],
|
||||
'targets': ['target2', 'target4'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['3'],
|
||||
'targets': ['target6'],
|
||||
'rule': 'anyof'
|
||||
}
|
||||
]
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_reuse_many_draggable_with_mupliples(self):
|
||||
"""Test reusable draggables with mupltiple draggables per target"""
|
||||
user_input = '[{"1":"target1"}, \
|
||||
{"2":"target2"},{"1":"target1"},{"2":"target4"},{"2":"target4"}, \
|
||||
{"3":"target6"}, {"4": "target3"}, {"5": "target4"}, \
|
||||
{"5": "target5"}, {"6": "target2"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['1', '4'],
|
||||
'targets': ['target1', 'target3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['2', '6'],
|
||||
'targets': ['target2', 'target4'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['5'],
|
||||
'targets': ['target4', 'target5'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['3'],
|
||||
'targets': ['target6'],
|
||||
'rule': 'anyof'
|
||||
}
|
||||
]
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_reuse_many_draggable_with_mupliples_wrong(self):
|
||||
"""Test reusable draggables with mupltiple draggables per target"""
|
||||
user_input = '[{"1":"target1"}, \
|
||||
{"2":"target2"},{"1":"target1"}, \
|
||||
{"2":"target3"}, \
|
||||
{"2":"target4"}, \
|
||||
{"3":"target6"}, {"4": "target3"}, {"5": "target4"}, \
|
||||
{"5": "target5"}, {"6": "target2"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['1', '4'],
|
||||
'targets': ['target1', 'target3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['2', '6'],
|
||||
'targets': ['target2', 'target4'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['5'],
|
||||
'targets': ['target4', 'target5'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['3'],
|
||||
'targets': ['target6'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
assert not draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_label_10_targets_with_a_b_c_false(self):
|
||||
"""Test reusable draggables (no mupltiple draggables per target)"""
|
||||
user_input = '[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"},{"a":"target4"},{"b":"target5"}, \
|
||||
{"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target1"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal'
|
||||
}
|
||||
]
|
||||
assert not draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_label_10_targets_with_a_b_c_(self):
|
||||
"""Test reusable draggables (no mupltiple draggables per target)"""
|
||||
user_input = '[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"},{"a":"target4"},{"b":"target5"}, \
|
||||
{"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target10"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal'
|
||||
}
|
||||
]
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_label_10_targets_with_a_b_c_multiple(self):
|
||||
"""Test reusable draggables (mupltiple draggables per target)"""
|
||||
user_input = '[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"},{"b":"target5"}, \
|
||||
{"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target1"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a', 'a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c', 'c', 'c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'anyof+number'
|
||||
}
|
||||
]
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_label_10_targets_with_a_b_c_multiple_false(self):
|
||||
"""Test reusable draggables (mupltiple draggables per target)"""
|
||||
user_input = '[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"},{"a":"target4"},{"b":"target5"}, \
|
||||
{"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target1"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a', 'a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c', 'c', 'c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'anyof+number'
|
||||
}
|
||||
]
|
||||
assert not draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_label_10_targets_with_a_b_c_reused(self):
|
||||
"""Test a b c in 10 labels reused"""
|
||||
user_input = '[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"},{"b":"target5"}, \
|
||||
{"c":"target6"}, {"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target10"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a'],
|
||||
'targets': ['target1', 'target10'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c', 'c', 'c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal+number'
|
||||
}
|
||||
]
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_label_10_targets_with_a_b_c_reused_false(self):
|
||||
"""Test a b c in 10 labels reused false"""
|
||||
user_input = '[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"},{"b":"target5"}, {"a":"target8"},\
|
||||
{"c":"target6"}, {"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target10"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a'],
|
||||
'targets': ['target1', 'target10'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c', 'c', 'c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal+number'
|
||||
}
|
||||
]
|
||||
assert not draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_mixed_reuse_and_not_reuse(self):
|
||||
"""Test reusable draggables """
|
||||
user_input = '[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"}, {"a":"target4"},\
|
||||
{"a":"target5"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'b'],
|
||||
'targets': ['target1', 'target2', 'target4', 'target5'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3'],
|
||||
'rule': 'exact'
|
||||
}
|
||||
]
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_mixed_reuse_and_not_reuse_number(self):
|
||||
"""Test reusable draggables with number """
|
||||
user_input = '[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"}, {"a":"target4"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a', 'b'],
|
||||
'targets': ['target1', 'target2', 'target4'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3'],
|
||||
'rule': 'exact'
|
||||
}
|
||||
]
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_mixed_reuse_and_not_reuse_number_false(self):
|
||||
"""Test reusable draggables with numbers, but wrong"""
|
||||
user_input = '[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"}, {"a":"target4"}, {"a":"target10"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a', 'b'],
|
||||
'targets': ['target1', 'target2', 'target4', 'target10'],
|
||||
'rule': 'anyof_number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3'],
|
||||
'rule': 'exact'
|
||||
}
|
||||
]
|
||||
assert not draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
def test_alternative_correct_answer(self):
|
||||
user_input = '[{"name_with_icon":"t1"},\
|
||||
{"name_with_icon":"t1"},{"name_with_icon":"t1"},{"name4":"t1"}, \
|
||||
{"name4":"t1"}]'
|
||||
correct_answer = [
|
||||
{'draggables': ['name4'], 'targets': ['t1', 't1'], 'rule': 'exact'},
|
||||
{'draggables': ['name_with_icon'], 'targets': ['t1', 't1', 't1'],
|
||||
'rule': 'exact'}
|
||||
]
|
||||
assert draganddrop.grade(user_input, correct_answer)
|
||||
|
||||
|
||||
class Test_DragAndDrop_Populate(unittest.TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
|
||||
def test_1(self):
|
||||
correct_answer = {'1': [[40, 10], 29], 'name_with_icon': [20, 20]}
|
||||
user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]'
|
||||
dnd = draganddrop.DragAndDrop(correct_answer, user_input)
|
||||
|
||||
correct_groups = [['1'], ['name_with_icon']]
|
||||
correct_positions = [{'exact': [[[40, 10], 29]]}, {'exact': [[20, 20]]}]
|
||||
user_groups = [['1'], ['name_with_icon']]
|
||||
user_positions = [{'user': [[10, 10]]}, {'user': [[20, 20]]}]
|
||||
|
||||
assert correct_groups == dnd.correct_groups
|
||||
assert correct_positions == dnd.correct_positions
|
||||
assert user_groups == dnd.user_groups
|
||||
assert user_positions == dnd.user_positions
|
||||
|
||||
|
||||
class Test_DraAndDrop_Compare_Positions(unittest.TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
|
||||
def test_1(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]')
|
||||
assert dnd.compare_positions(correct=[[1, 1], [2, 3]], user=[[2, 3], [1, 1]], flag='anyof')
|
||||
|
||||
def test_2a(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]')
|
||||
assert dnd.compare_positions(correct=[[1, 1], [2, 3]], user=[[2, 3], [1, 1]], flag='exact')
|
||||
|
||||
def test_2b(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]')
|
||||
assert not dnd.compare_positions(correct=[[1, 1], [2, 3]], user=[[2, 13], [1, 1]], flag='exact')
|
||||
|
||||
def test_3(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]')
|
||||
assert not dnd.compare_positions(correct=['a', 'b'], user=['a', 'b', 'c'], flag='anyof')
|
||||
|
||||
def test_4(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]')
|
||||
assert dnd.compare_positions(correct=['a', 'b', 'c'], user=['a', 'b'], flag='anyof')
|
||||
|
||||
def test_5(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]')
|
||||
assert not dnd.compare_positions(correct=['a', 'b', 'c'], user=['a', 'c', 'b'], flag='exact')
|
||||
|
||||
def test_6(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]')
|
||||
assert dnd.compare_positions(correct=['a', 'b', 'c'], user=['a', 'c', 'b'], flag='anyof')
|
||||
|
||||
def test_7(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]')
|
||||
assert not dnd.compare_positions(correct=['a', 'b', 'b'], user=['a', 'c', 'b'], flag='anyof')
|
||||
|
||||
|
||||
def suite(): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
|
||||
testcases = [Test_PositionsCompare,
|
||||
Test_DragAndDrop_Populate,
|
||||
Test_DragAndDrop_Grade,
|
||||
Test_DraAndDrop_Compare_Positions]
|
||||
suites = []
|
||||
for testcase in testcases:
|
||||
suites.append(unittest.TestLoader().loadTestsFromTestCase(testcase))
|
||||
return unittest.TestSuite(suites)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.TextTestRunner(verbosity=2).run(suite())
|
||||
@@ -71,10 +71,7 @@ def top_python_dirs(dirname):
|
||||
dirs = os.listdir(subdir)
|
||||
top_dirs.extend(d for d in dirs if os.path.isdir(os.path.join(subdir, d)))
|
||||
|
||||
# sandbox-packages module causes F0001: module-not-found error when running pylint
|
||||
# this will exclude sandbox-packages module from pylint execution
|
||||
# TODO: upgrade the functionality to run pylint tests on sandbox-packages module too.
|
||||
modules_to_remove = ['sandbox-packages', '__pycache__']
|
||||
modules_to_remove = ['__pycache__']
|
||||
for module in modules_to_remove:
|
||||
if module in top_dirs:
|
||||
top_dirs.remove(module)
|
||||
|
||||
@@ -11,12 +11,11 @@ pyparsing # Python Parsing module
|
||||
random2 # Implementation of random module that works identically under Python 2 and 3
|
||||
scipy # Math, science, and engineering library
|
||||
sympy # Symbolic math library
|
||||
codejail-includes # CodeJail manages execution of untrusted code in secure sandboxes.
|
||||
|
||||
# numpy>=1.17.0 caused failures in importing numpy in code-jail environment.
|
||||
# The issue will be investigated and fixed in https://openedx.atlassian.net/browse/BOM-2841.
|
||||
numpy>=1.16.0,<1.17.0
|
||||
|
||||
# Install these packages from the edx-platform working tree
|
||||
# NOTE: if you change code in these packages, you MUST change the version
|
||||
# number in its setup.py or the code WILL NOT be installed during deploy.
|
||||
-e common/lib/sandbox-packages
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
#
|
||||
# make upgrade
|
||||
#
|
||||
common/lib/sandbox-packages
|
||||
# via -r requirements/edx-sandbox/py38.in
|
||||
cffi==1.15.0
|
||||
# via cryptography
|
||||
chem==1.2.0
|
||||
@@ -14,6 +12,8 @@ click==8.1.3
|
||||
# via
|
||||
# -c requirements/edx-sandbox/../constraints.txt
|
||||
# nltk
|
||||
codejail-includes==1.0.0
|
||||
# via -r requirements/edx-sandbox/py38.in
|
||||
cryptography==37.0.2
|
||||
# via -r requirements/edx-sandbox/py38.in
|
||||
cycler==0.11.0
|
||||
@@ -82,6 +82,7 @@ scipy==1.7.3
|
||||
six==1.16.0
|
||||
# via
|
||||
# chem
|
||||
# codejail-includes
|
||||
# python-dateutil
|
||||
sympy==1.10.1
|
||||
# via
|
||||
|
||||
@@ -36,6 +36,7 @@ botocore==1.8.17 # via boto3, s3transfer
|
||||
bridgekeeper # Used for determining permissions for courseware.
|
||||
celery # Asynchronous task execution library
|
||||
chem # A helper library for chemistry calculations
|
||||
codejail-includes # CodeJail manages execution of untrusted code in secure sandboxes.
|
||||
contextlib2 # We need contextlib2.ExitStack so we can stop using contextlib.nested which doesn't exist in python 3
|
||||
crowdsourcehinter-xblock
|
||||
cryptography # Implementations of assorted cryptography algorithms
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
# via -r requirements/edx/local.in
|
||||
-e git+https://github.com/edx/RateXBlock.git@2.0.1#egg=rate-xblock
|
||||
# via -r requirements/edx/github.in
|
||||
-e common/lib/sandbox-packages
|
||||
# via -r requirements/edx/local.in
|
||||
-e openedx/core/lib/xblock_builtin/xblock_discussion
|
||||
# via -r requirements/edx/local.in
|
||||
-e git+https://github.com/edx-solutions/xblock-google-drive.git@2d176468e33c0713c911b563f8f65f7cf232f5b6#egg=xblock-google-drive
|
||||
@@ -143,6 +141,8 @@ code-annotations==1.3.0
|
||||
# via
|
||||
# edx-enterprise
|
||||
# edx-toggles
|
||||
codejail-includes==1.0.0
|
||||
# via -r requirements/edx/base.in
|
||||
contextlib2==21.6.0
|
||||
# via -r requirements/edx/base.in
|
||||
coreapi==2.3.3
|
||||
@@ -962,6 +962,7 @@ six==1.16.0
|
||||
# chem
|
||||
# click-repl
|
||||
# codejail
|
||||
# codejail-includes
|
||||
# crowdsourcehinter-xblock
|
||||
# edx-ace
|
||||
# edx-auth-backends
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
# via -r requirements/edx/testing.txt
|
||||
-e git+https://github.com/edx/RateXBlock.git@2.0.1#egg=rate-xblock
|
||||
# via -r requirements/edx/testing.txt
|
||||
-e common/lib/sandbox-packages
|
||||
# via -r requirements/edx/testing.txt
|
||||
-e openedx/core/lib/xblock_builtin/xblock_discussion
|
||||
# via -r requirements/edx/testing.txt
|
||||
-e git+https://github.com/edx-solutions/xblock-google-drive.git@2d176468e33c0713c911b563f8f65f7cf232f5b6#egg=xblock-google-drive
|
||||
@@ -203,6 +201,8 @@ code-annotations==1.3.0
|
||||
# edx-enterprise
|
||||
# edx-lint
|
||||
# edx-toggles
|
||||
codejail-includes==1.0.0
|
||||
# via -r requirements/edx/testing.txt
|
||||
contextlib2==21.6.0
|
||||
# via -r requirements/edx/testing.txt
|
||||
coreapi==2.3.3
|
||||
@@ -1334,6 +1334,7 @@ six==1.16.0
|
||||
# chem
|
||||
# click-repl
|
||||
# codejail
|
||||
# codejail-includes
|
||||
# crowdsourcehinter-xblock
|
||||
# edx-ace
|
||||
# edx-auth-backends
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Python libraries to install that are local to the edx-platform repo
|
||||
-e .
|
||||
-e common/lib/capa
|
||||
-e common/lib/sandbox-packages
|
||||
-e common/lib/xmodule
|
||||
|
||||
-e openedx/core/lib/xblock_builtin/xblock_discussion
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
# via -r requirements/edx/base.txt
|
||||
-e git+https://github.com/edx/RateXBlock.git@2.0.1#egg=rate-xblock
|
||||
# via -r requirements/edx/base.txt
|
||||
-e common/lib/sandbox-packages
|
||||
# via -r requirements/edx/base.txt
|
||||
-e openedx/core/lib/xblock_builtin/xblock_discussion
|
||||
# via -r requirements/edx/base.txt
|
||||
-e git+https://github.com/edx-solutions/xblock-google-drive.git@2d176468e33c0713c911b563f8f65f7cf232f5b6#egg=xblock-google-drive
|
||||
@@ -195,6 +193,8 @@ code-annotations==1.3.0
|
||||
# edx-enterprise
|
||||
# edx-lint
|
||||
# edx-toggles
|
||||
codejail-includes==1.0.0
|
||||
# via -r requirements/edx/base.txt
|
||||
contextlib2==21.6.0
|
||||
# via -r requirements/edx/base.txt
|
||||
coreapi==2.3.3
|
||||
@@ -1261,6 +1261,7 @@ six==1.16.0
|
||||
# chem
|
||||
# click-repl
|
||||
# codejail
|
||||
# codejail-includes
|
||||
# crowdsourcehinter-xblock
|
||||
# edx-ace
|
||||
# edx-auth-backends
|
||||
|
||||
@@ -38,7 +38,7 @@ exclude+='|^common/test/data/?.*$'
|
||||
# * common/lib/xmodule -> EXCLUDE from check.
|
||||
# * common/lib/xmodule/xmodule/modulestore -> INCLUDE in check.
|
||||
exclude+='|^common/lib$'
|
||||
exclude+='|^common/lib/(capa|sandbox-packages|xmodule)$'
|
||||
exclude+='|^common/lib/(capa|xmodule)$'
|
||||
|
||||
# Docs, scripts.
|
||||
exclude+='|^docs/.*$'
|
||||
|
||||
Reference in New Issue
Block a user