Merge pull request #1414 from MITx/feature/victor/showanswer-past-due
Feature/victor/showanswer past due
This commit is contained in:
@@ -389,11 +389,18 @@ class CapaModule(XModule):
|
||||
})
|
||||
return json.dumps(d, cls=ComplexEncoder)
|
||||
|
||||
def is_past_due(self):
|
||||
"""
|
||||
Is it now past this problem's due date, including grace period?
|
||||
"""
|
||||
return (self.close_date is not None and
|
||||
datetime.datetime.utcnow() > self.close_date)
|
||||
|
||||
def closed(self):
|
||||
''' Is the student still allowed to submit answers? '''
|
||||
if self.attempts == self.max_attempts:
|
||||
return True
|
||||
if self.close_date is not None and datetime.datetime.utcnow() > self.close_date:
|
||||
if self.is_past_due():
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -408,28 +415,28 @@ class CapaModule(XModule):
|
||||
return self.attempts > 0
|
||||
|
||||
def answer_available(self):
|
||||
''' Is the user allowed to see an answer?
|
||||
'''
|
||||
Is the user allowed to see an answer?
|
||||
'''
|
||||
if self.show_answer == '':
|
||||
return False
|
||||
|
||||
if self.show_answer == "never":
|
||||
elif self.show_answer == "never":
|
||||
return False
|
||||
|
||||
# Admins can see the answer, unless the problem explicitly prevents it
|
||||
if self.system.user_is_staff:
|
||||
elif self.system.user_is_staff:
|
||||
# This is after the 'never' check because admins can see the answer
|
||||
# unless the problem explicitly prevents it
|
||||
return True
|
||||
|
||||
if self.show_answer == 'attempted':
|
||||
elif self.show_answer == 'attempted':
|
||||
return self.attempts > 0
|
||||
|
||||
if self.show_answer == 'answered':
|
||||
elif self.show_answer == 'answered':
|
||||
# NOTE: this is slightly different from 'attempted' -- resetting the problems
|
||||
# makes lcp.done False, but leaves attempts unchanged.
|
||||
return self.lcp.done
|
||||
|
||||
if self.show_answer == 'closed':
|
||||
elif self.show_answer == 'closed':
|
||||
return self.closed()
|
||||
|
||||
if self.show_answer == 'always':
|
||||
elif self.show_answer == 'past_due':
|
||||
return self.is_past_due()
|
||||
elif self.show_answer == 'always':
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -678,18 +685,18 @@ class CapaDescriptor(RawDescriptor):
|
||||
# TODO (vshnayder): do problems have any other metadata? Do they
|
||||
# actually use type and points?
|
||||
metadata_attributes = RawDescriptor.metadata_attributes + ('type', 'points')
|
||||
|
||||
|
||||
def get_context(self):
|
||||
_context = RawDescriptor.get_context(self)
|
||||
_context.update({'markdown': self.metadata.get('markdown', '')})
|
||||
return _context
|
||||
|
||||
|
||||
@property
|
||||
def editable_metadata_fields(self):
|
||||
"""Remove metadata from the editable fields since it has its own editor"""
|
||||
subset = super(CapaDescriptor,self).editable_metadata_fields
|
||||
if 'markdown' in subset:
|
||||
subset.remove('markdown')
|
||||
subset.remove('markdown')
|
||||
return subset
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ test_system = ModuleSystem(
|
||||
# "render" to just the context...
|
||||
render_template=lambda template, context: str(context),
|
||||
replace_urls=Mock(),
|
||||
user=Mock(),
|
||||
user=Mock(is_staff=False),
|
||||
filestore=Mock(),
|
||||
debug=True,
|
||||
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10},
|
||||
|
||||
215
common/lib/xmodule/xmodule/tests/test_capa_module.py
Normal file
215
common/lib/xmodule/xmodule/tests/test_capa_module.py
Normal file
@@ -0,0 +1,215 @@
|
||||
import datetime
|
||||
import json
|
||||
from mock import Mock
|
||||
from pprint import pprint
|
||||
import unittest
|
||||
|
||||
from xmodule.capa_module import CapaModule
|
||||
from xmodule.modulestore import Location
|
||||
from lxml import etree
|
||||
|
||||
from . import test_system
|
||||
|
||||
class CapaFactory(object):
|
||||
"""
|
||||
A helper class to create problem modules with various parameters for testing.
|
||||
"""
|
||||
|
||||
sample_problem_xml = """<?xml version="1.0"?>
|
||||
<problem>
|
||||
<text>
|
||||
<p>What is pi, to two decimal placs?</p>
|
||||
</text>
|
||||
<numericalresponse answer="3.14">
|
||||
<textline math="1" size="30"/>
|
||||
</numericalresponse>
|
||||
</problem>
|
||||
"""
|
||||
|
||||
num = 0
|
||||
@staticmethod
|
||||
def next_num():
|
||||
CapaFactory.num += 1
|
||||
return CapaFactory.num
|
||||
|
||||
@staticmethod
|
||||
def create(graceperiod=None,
|
||||
due=None,
|
||||
max_attempts=None,
|
||||
showanswer=None,
|
||||
rerandomize=None,
|
||||
force_save_button=None,
|
||||
attempts=None,
|
||||
problem_state=None,
|
||||
):
|
||||
"""
|
||||
All parameters are optional, and are added to the created problem if specified.
|
||||
|
||||
Arguments:
|
||||
graceperiod:
|
||||
due:
|
||||
max_attempts:
|
||||
showanswer:
|
||||
force_save_button:
|
||||
rerandomize: all strings, as specified in the policy for the problem
|
||||
|
||||
problem_state: a dict to to be serialized into the instance_state of the
|
||||
module.
|
||||
|
||||
attempts: also added to instance state. Will be converted to an int.
|
||||
"""
|
||||
definition = {'data': CapaFactory.sample_problem_xml,}
|
||||
location = Location(["i4x", "edX", "capa_test", "problem",
|
||||
"SampleProblem{0}".format(CapaFactory.next_num())])
|
||||
metadata = {}
|
||||
if graceperiod is not None:
|
||||
metadata['graceperiod'] = graceperiod
|
||||
if due is not None:
|
||||
metadata['due'] = due
|
||||
if max_attempts is not None:
|
||||
metadata['attempts'] = max_attempts
|
||||
if showanswer is not None:
|
||||
metadata['showanswer'] = showanswer
|
||||
if force_save_button is not None:
|
||||
metadata['force_save_button'] = force_save_button
|
||||
if rerandomize is not None:
|
||||
metadata['rerandomize'] = rerandomize
|
||||
|
||||
|
||||
descriptor = Mock(weight="1")
|
||||
instance_state_dict = {}
|
||||
if problem_state is not None:
|
||||
instance_state_dict = problem_state
|
||||
if attempts is not None:
|
||||
# converting to int here because I keep putting "0" and "1" in the tests
|
||||
# since everything else is a string.
|
||||
instance_state_dict['attempts'] = int(attempts)
|
||||
if len(instance_state_dict) > 0:
|
||||
instance_state = json.dumps(instance_state_dict)
|
||||
else:
|
||||
instance_state = None
|
||||
|
||||
module = CapaModule(test_system, location,
|
||||
definition, descriptor,
|
||||
instance_state, None, metadata=metadata)
|
||||
|
||||
return module
|
||||
|
||||
|
||||
|
||||
class CapaModuleTest(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
now = datetime.datetime.now()
|
||||
day_delta = datetime.timedelta(days=1)
|
||||
self.yesterday_str = str(now - day_delta)
|
||||
self.today_str = str(now)
|
||||
self.tomorrow_str = str(now + day_delta)
|
||||
|
||||
# in the capa grace period format, not in time delta format
|
||||
self.two_day_delta_str = "2 days"
|
||||
|
||||
def test_import(self):
|
||||
module = CapaFactory.create()
|
||||
self.assertEqual(module.get_score()['score'], 0)
|
||||
|
||||
other_module = CapaFactory.create()
|
||||
self.assertEqual(module.get_score()['score'], 0)
|
||||
self.assertNotEqual(module.url_name, other_module.url_name,
|
||||
"Factory should be creating unique names for each problem")
|
||||
|
||||
def test_showanswer_default(self):
|
||||
"""
|
||||
Make sure the show answer logic does the right thing.
|
||||
"""
|
||||
# default, no due date, showanswer 'closed', so problem is open, and show_answer
|
||||
# not visible.
|
||||
problem = CapaFactory.create()
|
||||
self.assertFalse(problem.answer_available())
|
||||
|
||||
|
||||
def test_showanswer_attempted(self):
|
||||
problem = CapaFactory.create(showanswer='attempted')
|
||||
self.assertFalse(problem.answer_available())
|
||||
problem.attempts = 1
|
||||
self.assertTrue(problem.answer_available())
|
||||
|
||||
|
||||
def test_showanswer_closed(self):
|
||||
|
||||
# can see after attempts used up, even with due date in the future
|
||||
used_all_attempts = CapaFactory.create(showanswer='closed',
|
||||
max_attempts="1",
|
||||
attempts="1",
|
||||
due=self.tomorrow_str)
|
||||
self.assertTrue(used_all_attempts.answer_available())
|
||||
|
||||
|
||||
# can see after due date
|
||||
after_due_date = CapaFactory.create(showanswer='closed',
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.yesterday_str)
|
||||
self.assertTrue(after_due_date.answer_available())
|
||||
|
||||
|
||||
# can't see because attempts left
|
||||
attempts_left_open = CapaFactory.create(showanswer='closed',
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.tomorrow_str)
|
||||
self.assertFalse(attempts_left_open.answer_available())
|
||||
|
||||
# Can't see because grace period hasn't expired
|
||||
still_in_grace = CapaFactory.create(showanswer='closed',
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.yesterday_str,
|
||||
graceperiod=self.two_day_delta_str)
|
||||
self.assertFalse(still_in_grace.answer_available())
|
||||
|
||||
|
||||
|
||||
def test_showanswer_past_due(self):
|
||||
"""
|
||||
With showanswer="past_due" should only show answer after the problem is closed
|
||||
for everyone--e.g. after due date + grace period.
|
||||
"""
|
||||
|
||||
# can see after attempts used up, even with due date in the future
|
||||
used_all_attempts = CapaFactory.create(showanswer='past_due',
|
||||
max_attempts="1",
|
||||
attempts="1",
|
||||
due=self.tomorrow_str)
|
||||
self.assertFalse(used_all_attempts.answer_available())
|
||||
|
||||
|
||||
# can see after due date
|
||||
past_due_date = CapaFactory.create(showanswer='past_due',
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.yesterday_str)
|
||||
self.assertTrue(past_due_date.answer_available())
|
||||
|
||||
|
||||
# can't see because attempts left
|
||||
attempts_left_open = CapaFactory.create(showanswer='past_due',
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.tomorrow_str)
|
||||
self.assertFalse(attempts_left_open.answer_available())
|
||||
|
||||
# Can't see because grace period hasn't expired, even though have no more
|
||||
# attempts.
|
||||
still_in_grace = CapaFactory.create(showanswer='past_due',
|
||||
max_attempts="1",
|
||||
attempts="1",
|
||||
due=self.yesterday_str,
|
||||
graceperiod=self.two_day_delta_str)
|
||||
self.assertFalse(still_in_grace.answer_available())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user