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