diff --git a/brew-formulas.txt b/brew-formulas.txt
index b5b555e2a0..e06829a43a 100644
--- a/brew-formulas.txt
+++ b/brew-formulas.txt
@@ -1,10 +1,11 @@
-readline
-sqlite
-gdbm
-pkg-config
-gfortran
-python
-yuicompressor
+readline
+sqlite
+gdbm
+pkg-config
+gfortran
+python
+yuicompressor
node
graphviz
mysql
+geos
diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py
index b990c489b3..20e7c43577 100644
--- a/common/lib/capa/capa/responsetypes.py
+++ b/common/lib/capa/capa/responsetypes.py
@@ -23,6 +23,7 @@ import abc
import os
import subprocess
import xml.sax.saxutils as saxutils
+from shapely.geometry import Point, MultiPoint
# specific library imports
from calc import evaluator, UndefinedVariable
@@ -1717,15 +1718,38 @@ class ImageResponse(LoncapaResponse):
which produces an [x,y] coordinate pair. The click is correct if it falls
within a region specified. This region is a union of rectangles.
- Lon-CAPA requires that each has a inside it. That
- doesn't make sense to me (Ike). Instead, let's have it such that
- should contain one or more stanzas. Each should specify
- a rectangle, given as an attribute, defining the correct answer.
+ Lon-CAPA requires that each has a inside it.
+ That doesn't make sense to me (Ike). Instead, let's have it such that
+ should contain one or more stanzas.
+ Each should specify a rectangle(s) or region(s), given as an
+ attribute, defining the correct answer.
+
+
+
+ Regions is list of lists [region1, region2, region3, ...] where regionN
+ is disordered list of points: [[1,1], [100,100], [50,50], [20, 70]].
+
+ If there is only one region in the list, simpler notation can be used:
+ regions="[[10,10], [30,30], [10, 30], [30, 10]]" (without explicitly
+ setting outer list)
+
+ Returns:
+ True, if click is inside any region or rectangle. Otherwise False.
"""
snippets = [{'snippet': '''
-
-
-
+
+
+
+
+
'''}]
response_tag = 'imageresponse'
@@ -1733,19 +1757,17 @@ class ImageResponse(LoncapaResponse):
def setup_response(self):
self.ielements = self.inputfields
- self.answer_ids = [ie.get('id') for ie in self.ielements]
+ self.answer_ids = [ie.get('id') for ie in self.ielements]
def get_score(self, student_answers):
correct_map = CorrectMap()
expectedset = self.get_answers()
-
- for aid in self.answer_ids: # loop through IDs of fields in our stanza
- given = student_answers[aid] # this should be a string of the form '[x,y]'
-
+ for aid in self.answer_ids: # loop through IDs of
+ # fields in our stanza
+ given = student_answers[aid] # this should be a string of the form '[x,y]'
correct_map.set(aid, 'incorrect')
- if not given: # No answer to parse. Mark as incorrect and move on
+ if not given: # No answer to parse. Mark as incorrect and move on
continue
-
# parse given answer
m = re.match('\[([0-9]+),([0-9]+)]', given.strip().replace(' ', ''))
if not m:
@@ -1753,29 +1775,44 @@ class ImageResponse(LoncapaResponse):
'error grading %s (input=%s)' % (aid, given))
(gx, gy) = [int(x) for x in m.groups()]
- # Check whether given point lies in any of the solution rectangles
- solution_rectangles = expectedset[aid].split(';')
- for solution_rectangle in solution_rectangles:
- # parse expected answer
- # TODO: Compile regexp on file load
- m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',
- solution_rectangle.strip().replace(' ', ''))
- if not m:
- msg = 'Error in problem specification! cannot parse rectangle in %s' % (
- etree.tostring(self.ielements[aid], pretty_print=True))
- raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg)
- (llx, lly, urx, ury) = [int(x) for x in m.groups()]
-
- # answer is correct if (x,y) is within the specified rectangle
- if (llx <= gx <= urx) and (lly <= gy <= ury):
- correct_map.set(aid, 'correct')
- break
+ rectangles, regions = expectedset
+ if rectangles[aid]: # rectangles part - for backward compatibility
+ # Check whether given point lies in any of the solution rectangles
+ solution_rectangles = rectangles[aid].split(';')
+ for solution_rectangle in solution_rectangles:
+ # parse expected answer
+ # TODO: Compile regexp on file load
+ m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',
+ solution_rectangle.strip().replace(' ', ''))
+ if not m:
+ msg = 'Error in problem specification! cannot parse rectangle in %s' % (
+ etree.tostring(self.ielements[aid], pretty_print=True))
+ raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg)
+ (llx, lly, urx, ury) = [int(x) for x in m.groups()]
+ # answer is correct if (x,y) is within the specified rectangle
+ if (llx <= gx <= urx) and (lly <= gy <= ury):
+ correct_map.set(aid, 'correct')
+ break
+ if correct_map[aid]['correctness'] != 'correct' and regions[aid]:
+ parsed_region = json.loads(regions[aid])
+ if parsed_region:
+ if type(parsed_region[0][0]) != list:
+ # we have [[1,2],[3,4],[5,6]] - single region
+ # instead of [[[1,2],[3,4],[5,6], [[1,2],[3,4],[5,6]]]
+ # or [[[1,2],[3,4],[5,6]]] - multiple regions syntax
+ parsed_region = [parsed_region]
+ for region in parsed_region:
+ polygon = MultiPoint(region).convex_hull
+ if (polygon.type == 'Polygon' and
+ polygon.contains(Point(gx, gy))):
+ correct_map.set(aid, 'correct')
+ break
return correct_map
def get_answers(self):
- return dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements])
-
+ return (dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements]),
+ dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements]))
#-----------------------------------------------------------------------------
# TEMPORARY: List of all response subclasses
# FIXME: To be replaced by auto-registration
diff --git a/common/lib/capa/capa/tests/test_files/imageresponse.xml b/common/lib/capa/capa/tests/test_files/imageresponse.xml
index 34dba37e3b..41c9f01218 100644
--- a/common/lib/capa/capa/tests/test_files/imageresponse.xml
+++ b/common/lib/capa/capa/tests/test_files/imageresponse.xml
@@ -18,4 +18,23 @@ Hello
Use conservation of energy.
+
+
+
+
+
+
+
+Click on either of the two positions as discussed previously.
+
+Click on either of the two positions as discussed previously.
+
+
+Click on either of the two positions as discussed previously.
+
+Use conservation of energy.
+
+
+
+
diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py
index bcac555b5e..9eecef3986 100644
--- a/common/lib/capa/capa/tests/test_responsetypes.py
+++ b/common/lib/capa/capa/tests/test_responsetypes.py
@@ -52,24 +52,57 @@ class ImageResponseTest(unittest.TestCase):
def test_ir_grade(self):
imageresponse_file = os.path.dirname(__file__) + "/test_files/imageresponse.xml"
test_lcp = lcp.LoncapaProblem(open(imageresponse_file).read(), '1', system=test_system)
- correct_answers = {'1_2_1': '(490,11)-(556,98)',
- '1_2_2': '(242,202)-(296,276)',
- '1_2_3': '(490,11)-(556,98);(242,202)-(296,276)',
- '1_2_4': '(490,11)-(556,98);(242,202)-(296,276)',
- '1_2_5': '(490,11)-(556,98);(242,202)-(296,276)',
+ # testing regions only
+ correct_answers = {
+ #regions
+ '1_2_1': '(490,11)-(556,98)',
+ '1_2_2': '(242,202)-(296,276)',
+ '1_2_3': '(490,11)-(556,98);(242,202)-(296,276)',
+ '1_2_4': '(490,11)-(556,98);(242,202)-(296,276)',
+ '1_2_5': '(490,11)-(556,98);(242,202)-(296,276)',
+ #testing regions and rectanges
+ '1_3_1': 'rectangle="(490,11)-(556,98)" \
+ regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"',
+ '1_3_2': 'rectangle="(490,11)-(556,98)" \
+ regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"',
+ '1_3_3': 'regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"',
+ '1_3_4': 'regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"',
+ '1_3_5': 'regions="[[[10,10], [20,10], [20, 30]]]"',
+ '1_3_6': 'regions="[[10,10], [30,30], [15, 15]]"',
+ '1_3_7': 'regions="[[10,10], [30,30], [10, 30], [30, 10]]"',
}
- test_answers = {'1_2_1': '[500,20]',
- '1_2_2': '[250,300]',
- '1_2_3': '[500,20]',
- '1_2_4': '[250,250]',
- '1_2_5': '[10,10]',
+ test_answers = {
+ '1_2_1': '[500,20]',
+ '1_2_2': '[250,300]',
+ '1_2_3': '[500,20]',
+ '1_2_4': '[250,250]',
+ '1_2_5': '[10,10]',
+
+ '1_3_1': '[500,20]',
+ '1_3_2': '[15,15]',
+ '1_3_3': '[500,20]',
+ '1_3_4': '[115,115]',
+ '1_3_5': '[15,15]',
+ '1_3_6': '[20,20]',
+ '1_3_7': '[20,15]',
}
+
+ # regions
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_3'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_4'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_5'), 'incorrect')
+ # regions and rectangles
+ self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'correct')
+ self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_2'), 'correct')
+ self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_3'), 'incorrect')
+ self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_4'), 'correct')
+ self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_5'), 'correct')
+ self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_6'), 'incorrect')
+ self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_7'), 'correct')
+
class SymbolicResponseTest(unittest.TestCase):
def test_sr_grade(self):
diff --git a/create-dev-env.sh b/create-dev-env.sh
index e481d3fd5e..5edc765e4f 100755
--- a/create-dev-env.sh
+++ b/create-dev-env.sh
@@ -99,7 +99,7 @@ NUMPY_VER="1.6.2"
SCIPY_VER="0.10.1"
BREW_FILE="$BASE/mitx/brew-formulas.txt"
LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log"
-APT_PKGS="pkg-config curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor nodejs npm graphviz graphviz-dev mysql-server libmysqlclient-dev"
+APT_PKGS="pkg-config curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor nodejs npm graphviz graphviz-dev mysql-server libmysqlclient-dev libgeos-dev"
if [[ $EUID -eq 0 ]]; then
error "This script should not be run using sudo or as the root user"
diff --git a/requirements.txt b/requirements.txt
index 28b12404a1..ac50bd1691 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,11 +8,11 @@ lxml
boto
mako
python-memcached
-python-openid
+python-openid
path.py
django_debug_toolbar
fs
-beautifulsoup
+beautifulsoup
beautifulsoup4
feedparser
requests
@@ -37,7 +37,7 @@ django-jasmine
django-keyedcache
django-mako
django-masquerade
-django-openid-auth
+django-openid-auth
django-robots
django-ses
django-storages
@@ -54,3 +54,4 @@ dogstatsd-python
# Taking out MySQL-python for now because it requires mysql to be installed, so breaks updates on content folks' envs.
# MySQL-python
sphinx
+Shapely