Commit 62dcd09b by VikParuchuri

Merge pull request #1428 from MITx/feature/diana/close-oe-problems

Close OE Problems
parents 53312713 f3fa4380
...@@ -7,7 +7,11 @@ from lxml import etree ...@@ -7,7 +7,11 @@ from lxml import etree
from lxml.html import rewrite_links from lxml.html import rewrite_links
from path import path from path import path
import os import os
import dateutil
import dateutil.parser
import datetime
import sys import sys
from timeparse import parse_timedelta
from pkg_resources import resource_string from pkg_resources import resource_string
...@@ -157,12 +161,35 @@ class CombinedOpenEndedModule(XModule): ...@@ -157,12 +161,35 @@ class CombinedOpenEndedModule(XModule):
self.attempts = instance_state.get('attempts', 0) self.attempts = instance_state.get('attempts', 0)
#Allow reset is true if student has failed the criteria to move to the next child task #Allow reset is true if student has failed the criteria to move to the next child task
self.allow_reset = instance_state.get('ready_to_reset', False) self.allow_reset = instance_state.get('ready_to_reset', False)
self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS)) self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS))
self.is_scored = self.metadata.get('is_graded', IS_SCORED) in TRUE_DICT self.is_scored = self.metadata.get('is_graded', IS_SCORED) in TRUE_DICT
self.accept_file_upload = self.metadata.get('accept_file_upload', ACCEPT_FILE_UPLOAD) in TRUE_DICT self.accept_file_upload = self.metadata.get('accept_file_upload', ACCEPT_FILE_UPLOAD) in TRUE_DICT
display_due_date_string = self.metadata.get('due', None)
if display_due_date_string is not None:
try:
self.display_due_date = dateutil.parser.parse(display_due_date_string)
except ValueError:
log.error("Could not parse due date {0} for location {1}".format(display_due_date_string, location))
raise
else:
self.display_due_date = None
grace_period_string = self.metadata.get('graceperiod', None)
if grace_period_string is not None and self.display_due_date:
try:
self.grace_period = parse_timedelta(grace_period_string)
self.close_date = self.display_due_date + self.grace_period
except:
log.error("Error parsing the grace period {0} for location {1}".format(grace_period_string, location))
raise
else:
self.grace_period = None
self.close_date = self.display_due_date
# Used for progress / grading. Currently get credit just for # Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect). # completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = int(self.metadata.get('max_score', MAX_SCORE)) self._max_score = int(self.metadata.get('max_score', MAX_SCORE))
...@@ -185,11 +212,13 @@ class CombinedOpenEndedModule(XModule): ...@@ -185,11 +212,13 @@ class CombinedOpenEndedModule(XModule):
'rubric': definition['rubric'], 'rubric': definition['rubric'],
'display_name': self.display_name, 'display_name': self.display_name,
'accept_file_upload': self.accept_file_upload, 'accept_file_upload': self.accept_file_upload,
'close_date': self.close_date
} }
self.task_xml = definition['task_xml'] self.task_xml = definition['task_xml']
self.setup_next_task() self.setup_next_task()
def get_tag_name(self, xml): def get_tag_name(self, xml):
""" """
Gets the tag name of a given xml block. Gets the tag name of a given xml block.
...@@ -299,6 +328,7 @@ class CombinedOpenEndedModule(XModule): ...@@ -299,6 +328,7 @@ class CombinedOpenEndedModule(XModule):
return True return True
def check_allow_reset(self): def check_allow_reset(self):
""" """
Checks to see if the student has passed the criteria to move to the next module. If not, sets Checks to see if the student has passed the criteria to move to the next module. If not, sets
......
...@@ -549,14 +549,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -549,14 +549,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
@param system: modulesystem @param system: modulesystem
@return: Success indicator @return: Success indicator
""" """
if self.attempts > self.max_attempts: # Once we close the problem, we should not allow students
# If too many attempts, prevent student from saving answer and # to save answers
# seeing rubric. In normal use, students shouldn't see this because closed, msg = self.check_if_closed()
# they won't see the reset button once they're out of attempts. if closed:
return { return msg
'success': False,
'error': 'Too many attempts.'
}
if self.state != self.INITIAL: if self.state != self.INITIAL:
return self.out_of_sync_error(get) return self.out_of_sync_error(get)
......
...@@ -74,7 +74,7 @@ class OpenEndedChild(object): ...@@ -74,7 +74,7 @@ class OpenEndedChild(object):
'done': 'Problem complete', 'done': 'Problem complete',
} }
def __init__(self, system, location, definition, descriptor, static_data, def __init__(self, system, location, definition, descriptor, static_data,
instance_state=None, shared_state=None, **kwargs): instance_state=None, shared_state=None, **kwargs):
# Load instance state # Load instance state
if instance_state is not None: if instance_state is not None:
...@@ -99,6 +99,7 @@ class OpenEndedChild(object): ...@@ -99,6 +99,7 @@ class OpenEndedChild(object):
self.rubric = static_data['rubric'] self.rubric = static_data['rubric']
self.display_name = static_data['display_name'] self.display_name = static_data['display_name']
self.accept_file_upload = static_data['accept_file_upload'] self.accept_file_upload = static_data['accept_file_upload']
self.close_date = static_data['close_date']
# Used for progress / grading. Currently get credit just for # Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect). # completion (doesn't matter if you self-assessed correct/incorrect).
...@@ -117,6 +118,27 @@ class OpenEndedChild(object): ...@@ -117,6 +118,27 @@ class OpenEndedChild(object):
""" """
pass pass
def closed(self):
if self.close_date is not None and datetime.utcnow() > self.close_date:
return True
return False
def check_if_closed(self):
if self.closed():
return True, {
'success': False,
'error': 'This problem is now closed.'
}
elif self.attempts > self.max_attempts:
return True, {
'success': False,
'error': 'Too many attempts.'
}
else:
return False, {}
def latest_answer(self): def latest_answer(self):
"""Empty string if not available""" """Empty string if not available"""
if not self.history: if not self.history:
......
...@@ -190,15 +190,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -190,15 +190,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
Dictionary with keys 'success' and either 'error' (if not success), Dictionary with keys 'success' and either 'error' (if not success),
or 'rubric_html' (if success). or 'rubric_html' (if success).
""" """
# Check to see if attempts are less than max # Check to see if this problem is closed
if self.attempts > self.max_attempts: closed, msg = self.check_if_closed()
# If too many attempts, prevent student from saving answer and if closed:
# seeing rubric. In normal use, students shouldn't see this because return msg
# they won't see the reset button once they're out of attempts.
return {
'success': False,
'error': 'Too many attempts.'
}
if self.state != self.INITIAL: if self.state != self.INITIAL:
return self.out_of_sync_error(get) return self.out_of_sync_error(get)
......
...@@ -42,6 +42,7 @@ class OpenEndedChildTest(unittest.TestCase): ...@@ -42,6 +42,7 @@ class OpenEndedChildTest(unittest.TestCase):
'max_score': max_score, 'max_score': max_score,
'display_name': 'Name', 'display_name': 'Name',
'accept_file_upload': False, 'accept_file_upload': False,
'close_date': None
} }
definition = Mock() definition = Mock()
descriptor = Mock() descriptor = Mock()
...@@ -157,6 +158,7 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -157,6 +158,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'max_score': max_score, 'max_score': max_score,
'display_name': 'Name', 'display_name': 'Name',
'accept_file_upload': False, 'accept_file_upload': False,
'close_date': None
} }
oeparam = etree.XML(''' oeparam = etree.XML('''
......
...@@ -46,11 +46,13 @@ class SelfAssessmentTest(unittest.TestCase): ...@@ -46,11 +46,13 @@ class SelfAssessmentTest(unittest.TestCase):
'max_score': 1, 'max_score': 1,
'display_name': "Name", 'display_name': "Name",
'accept_file_upload': False, 'accept_file_upload': False,
'close_date': None
} }
self.module = SelfAssessmentModule(test_system, self.location, self.module = SelfAssessmentModule(test_system, self.location,
self.definition, self.descriptor, self.definition, self.descriptor,
static_data, state, metadata=self.metadata) static_data,
state, metadata=self.metadata)
def test_get_html(self): def test_get_html(self):
html = self.module.get_html(test_system) html = self.module.get_html(test_system)
......
...@@ -2,9 +2,12 @@ ...@@ -2,9 +2,12 @@
Helper functions for handling time in the format we like. Helper functions for handling time in the format we like.
""" """
import time import time
import re
from datetime import timedelta
TIME_FORMAT = "%Y-%m-%dT%H:%M" TIME_FORMAT = "%Y-%m-%dT%H:%M"
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
def parse_time(time_str): def parse_time(time_str):
""" """
...@@ -22,3 +25,23 @@ def stringify_time(time_struct): ...@@ -22,3 +25,23 @@ def stringify_time(time_struct):
Convert a time struct to a string Convert a time struct to a string
""" """
return time.strftime(TIME_FORMAT, time_struct) return time.strftime(TIME_FORMAT, time_struct)
def parse_timedelta(time_str):
"""
time_str: A string with the following components:
<D> day[s] (optional)
<H> hour[s] (optional)
<M> minute[s] (optional)
<S> second[s] (optional)
Returns a datetime.timedelta parsed from the string
"""
parts = TIMEDELTA_REGEX.match(time_str)
if not parts:
return
parts = parts.groupdict()
time_params = {}
for (name, param) in parts.iteritems():
if param:
time_params[name] = int(param)
return timedelta(**time_params)
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