Commit ba9280f3 by Victor Shnayder

Merge pull request #1010 from MITx/feature/alex/regions

Feature/alex/regions
parents b63a787a b0a85fde
readline readline
sqlite sqlite
gdbm gdbm
pkg-config pkg-config
gfortran gfortran
python python
yuicompressor yuicompressor
node node
graphviz graphviz
mysql mysql
geos
...@@ -23,6 +23,7 @@ import abc ...@@ -23,6 +23,7 @@ import abc
import os import os
import subprocess import subprocess
import xml.sax.saxutils as saxutils import xml.sax.saxutils as saxutils
from shapely.geometry import Point, MultiPoint
# specific library imports # specific library imports
from calc import evaluator, UndefinedVariable from calc import evaluator, UndefinedVariable
...@@ -1717,15 +1718,38 @@ class ImageResponse(LoncapaResponse): ...@@ -1717,15 +1718,38 @@ class ImageResponse(LoncapaResponse):
which produces an [x,y] coordinate pair. The click is correct if it falls 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. within a region specified. This region is a union of rectangles.
Lon-CAPA requires that each <imageresponse> has a <foilgroup> inside it. That Lon-CAPA requires that each <imageresponse> has a <foilgroup> inside it.
doesn't make sense to me (Ike). Instead, let's have it such that <imageresponse> That doesn't make sense to me (Ike). Instead, let's have it such that
should contain one or more <imageinput> stanzas. Each <imageinput> should specify <imageresponse> should contain one or more <imageinput> stanzas.
a rectangle, given as an attribute, defining the correct answer. Each <imageinput> should specify a rectangle(s) or region(s), given as an
attribute, defining the correct answer.
<imageinput src="/static/images/Lecture2/S2_p04.png" width="811" height="610"
rectangle="(10,10)-(20,30);(12,12)-(40,60)"
regions="[[[10,10], [20,30], [40, 10]], [[100,100], [120,130], [110,150]]]"/>
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': '''<imageresponse> snippets = [{'snippet': '''<imageresponse>
<imageinput src="image1.jpg" width="200" height="100" rectangle="(10,10)-(20,30)" /> <imageinput src="image1.jpg" width="200" height="100"
<imageinput src="image2.jpg" width="210" height="130" rectangle="(12,12)-(40,60)" /> rectangle="(10,10)-(20,30)" />
<imageinput src="image2.jpg" width="210" height="130" rectangle="(10,10)-(20,30);(12,12)-(40,60)" /> <imageinput src="image2.jpg" width="210" height="130"
rectangle="(12,12)-(40,60)" />
<imageinput src="image3.jpg" width="210" height="130"
rectangle="(10,10)-(20,30);(12,12)-(40,60)" />
<imageinput src="image4.jpg" width="811" height="610"
rectangle="(10,10)-(20,30);(12,12)-(40,60)"
regions="[[[10,10], [20,30], [40, 10]], [[100,100], [120,130], [110,150]]]"/>
<imageinput src="image5.jpg" width="200" height="200"
regions="[[[10,10], [20,30], [40, 10]], [[100,100], [120,130], [110,150]]]"/>
</imageresponse>'''}] </imageresponse>'''}]
response_tag = 'imageresponse' response_tag = 'imageresponse'
...@@ -1733,19 +1757,17 @@ class ImageResponse(LoncapaResponse): ...@@ -1733,19 +1757,17 @@ class ImageResponse(LoncapaResponse):
def setup_response(self): def setup_response(self):
self.ielements = self.inputfields 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): def get_score(self, student_answers):
correct_map = CorrectMap() correct_map = CorrectMap()
expectedset = self.get_answers() expectedset = self.get_answers()
for aid in self.answer_ids: # loop through IDs of <imageinput>
for aid in self.answer_ids: # loop through IDs of <imageinput> fields in our stanza # fields in our stanza
given = student_answers[aid] # this should be a string of the form '[x,y]' given = student_answers[aid] # this should be a string of the form '[x,y]'
correct_map.set(aid, 'incorrect') 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 continue
# parse given answer # parse given answer
m = re.match('\[([0-9]+),([0-9]+)]', given.strip().replace(' ', '')) m = re.match('\[([0-9]+),([0-9]+)]', given.strip().replace(' ', ''))
if not m: if not m:
...@@ -1753,29 +1775,44 @@ class ImageResponse(LoncapaResponse): ...@@ -1753,29 +1775,44 @@ class ImageResponse(LoncapaResponse):
'error grading %s (input=%s)' % (aid, given)) 'error grading %s (input=%s)' % (aid, given))
(gx, gy) = [int(x) for x in m.groups()] (gx, gy) = [int(x) for x in m.groups()]
# Check whether given point lies in any of the solution rectangles rectangles, regions = expectedset
solution_rectangles = expectedset[aid].split(';') if rectangles[aid]: # rectangles part - for backward compatibility
for solution_rectangle in solution_rectangles: # Check whether given point lies in any of the solution rectangles
# parse expected answer solution_rectangles = rectangles[aid].split(';')
# TODO: Compile regexp on file load for solution_rectangle in solution_rectangles:
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', # parse expected answer
solution_rectangle.strip().replace(' ', '')) # TODO: Compile regexp on file load
if not m: m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',
msg = 'Error in problem specification! cannot parse rectangle in %s' % ( solution_rectangle.strip().replace(' ', ''))
etree.tostring(self.ielements[aid], pretty_print=True)) if not m:
raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg) msg = 'Error in problem specification! cannot parse rectangle in %s' % (
(llx, lly, urx, ury) = [int(x) for x in m.groups()] etree.tostring(self.ielements[aid], pretty_print=True))
raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg)
# answer is correct if (x,y) is within the specified rectangle (llx, lly, urx, ury) = [int(x) for x in m.groups()]
if (llx <= gx <= urx) and (lly <= gy <= ury):
correct_map.set(aid, 'correct') # answer is correct if (x,y) is within the specified rectangle
break 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 return correct_map
def get_answers(self): 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 # TEMPORARY: List of all response subclasses
# FIXME: To be replaced by auto-registration # FIXME: To be replaced by auto-registration
......
...@@ -18,4 +18,23 @@ Hello</p></text> ...@@ -18,4 +18,23 @@ Hello</p></text>
<text><p>Use conservation of energy.</p></text> <text><p>Use conservation of energy.</p></text>
</hintgroup> </hintgroup>
</imageresponse> </imageresponse>
<imageresponse max="1" loncapaid="12">
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98)" regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"/>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98)" regions='[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]'/>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"/>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"/>
<text>Click on either of the two positions as discussed previously.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[[10,10], [20,10], [20, 30]]]"/>
<text>Click on either of the two positions as discussed previously.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[10,10], [30,30], [15, 15]]"/>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[10,10], [30,30], [10, 30], [30, 10]]"/>
<text>Click on either of the two positions as discussed previously.</text>
<hintgroup showoncorrect="no">
<text><p>Use conservation of energy.</p></text>
</hintgroup>
</imageresponse>
</problem> </problem>
...@@ -52,24 +52,57 @@ class ImageResponseTest(unittest.TestCase): ...@@ -52,24 +52,57 @@ class ImageResponseTest(unittest.TestCase):
def test_ir_grade(self): def test_ir_grade(self):
imageresponse_file = os.path.dirname(__file__) + "/test_files/imageresponse.xml" imageresponse_file = os.path.dirname(__file__) + "/test_files/imageresponse.xml"
test_lcp = lcp.LoncapaProblem(open(imageresponse_file).read(), '1', system=test_system) test_lcp = lcp.LoncapaProblem(open(imageresponse_file).read(), '1', system=test_system)
correct_answers = {'1_2_1': '(490,11)-(556,98)', # testing regions only
'1_2_2': '(242,202)-(296,276)', correct_answers = {
'1_2_3': '(490,11)-(556,98);(242,202)-(296,276)', #regions
'1_2_4': '(490,11)-(556,98);(242,202)-(296,276)', '1_2_1': '(490,11)-(556,98)',
'1_2_5': '(490,11)-(556,98);(242,202)-(296,276)', '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]', test_answers = {
'1_2_2': '[250,300]', '1_2_1': '[500,20]',
'1_2_3': '[500,20]', '1_2_2': '[250,300]',
'1_2_4': '[250,250]', '1_2_3': '[500,20]',
'1_2_5': '[10,10]', '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_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_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_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_4'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_5'), 'incorrect') 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): class SymbolicResponseTest(unittest.TestCase):
def test_sr_grade(self): def test_sr_grade(self):
......
...@@ -99,7 +99,7 @@ NUMPY_VER="1.6.2" ...@@ -99,7 +99,7 @@ NUMPY_VER="1.6.2"
SCIPY_VER="0.10.1" SCIPY_VER="0.10.1"
BREW_FILE="$BASE/mitx/brew-formulas.txt" BREW_FILE="$BASE/mitx/brew-formulas.txt"
LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log" 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 if [[ $EUID -eq 0 ]]; then
error "This script should not be run using sudo or as the root user" error "This script should not be run using sudo or as the root user"
......
...@@ -8,11 +8,11 @@ lxml ...@@ -8,11 +8,11 @@ lxml
boto boto
mako mako
python-memcached python-memcached
python-openid python-openid
path.py path.py
django_debug_toolbar django_debug_toolbar
fs fs
beautifulsoup beautifulsoup
beautifulsoup4 beautifulsoup4
feedparser feedparser
requests requests
...@@ -37,7 +37,7 @@ django-jasmine ...@@ -37,7 +37,7 @@ django-jasmine
django-keyedcache django-keyedcache
django-mako django-mako
django-masquerade django-masquerade
django-openid-auth django-openid-auth
django-robots django-robots
django-ses django-ses
django-storages django-storages
...@@ -54,3 +54,4 @@ dogstatsd-python ...@@ -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. # Taking out MySQL-python for now because it requires mysql to be installed, so breaks updates on content folks' envs.
# MySQL-python # MySQL-python
sphinx sphinx
Shapely
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment