diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 112a7a68f8..69e7f03d89 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -7,7 +7,11 @@ from lxml import etree from lxml.html import rewrite_links from path import path import os +import dateutil +import dateutil.parser +import datetime import sys +from timeparse import parse_timedelta from pkg_resources import resource_string @@ -157,12 +161,35 @@ class CombinedOpenEndedModule(XModule): self.attempts = instance_state.get('attempts', 0) + #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.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS)) 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 + 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 # completion (doesn't matter if you self-assessed correct/incorrect). self._max_score = int(self.metadata.get('max_score', MAX_SCORE)) @@ -185,11 +212,13 @@ class CombinedOpenEndedModule(XModule): 'rubric': definition['rubric'], 'display_name': self.display_name, 'accept_file_upload': self.accept_file_upload, + 'close_date': self.close_date } self.task_xml = definition['task_xml'] self.setup_next_task() + def get_tag_name(self, xml): """ Gets the tag name of a given xml block. @@ -299,6 +328,7 @@ class CombinedOpenEndedModule(XModule): return True + def check_allow_reset(self): """ Checks to see if the student has passed the criteria to move to the next module. If not, sets diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 072a7153fb..0ad6a26995 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -549,14 +549,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): @param system: modulesystem @return: Success indicator """ - if self.attempts > self.max_attempts: - # If too many attempts, prevent student from saving answer and - # seeing rubric. In normal use, students shouldn't see this because - # they won't see the reset button once they're out of attempts. - return { - 'success': False, - 'error': 'Too many attempts.' - } + # Once we close the problem, we should not allow students + # to save answers + closed, msg = self.check_if_closed() + if closed: + return msg if self.state != self.INITIAL: return self.out_of_sync_error(get) diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index a56937270b..d9049c4f1e 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -74,7 +74,7 @@ class OpenEndedChild(object): '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): # Load instance state if instance_state is not None: @@ -99,6 +99,7 @@ class OpenEndedChild(object): self.rubric = static_data['rubric'] self.display_name = static_data['display_name'] 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 # completion (doesn't matter if you self-assessed correct/incorrect). @@ -117,6 +118,27 @@ class OpenEndedChild(object): """ 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): """Empty string if not available""" if not self.history: diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 07cc68a83a..367bb9aef6 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -190,15 +190,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): Dictionary with keys 'success' and either 'error' (if not success), or 'rubric_html' (if success). """ - # Check to see if attempts are less than max - if self.attempts > self.max_attempts: - # If too many attempts, prevent student from saving answer and - # seeing rubric. In normal use, students shouldn't see this because - # they won't see the reset button once they're out of attempts. - return { - 'success': False, - 'error': 'Too many attempts.' - } + # Check to see if this problem is closed + closed, msg = self.check_if_closed() + if closed: + return msg if self.state != self.INITIAL: return self.out_of_sync_error(get) diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py index 690eb7e39e..8b0a4f7c2f 100644 --- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py +++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py @@ -42,6 +42,7 @@ class OpenEndedChildTest(unittest.TestCase): 'max_score': max_score, 'display_name': 'Name', 'accept_file_upload': False, + 'close_date': None } definition = Mock() descriptor = Mock() @@ -157,6 +158,7 @@ class OpenEndedModuleTest(unittest.TestCase): 'max_score': max_score, 'display_name': 'Name', 'accept_file_upload': False, + 'close_date': None } oeparam = etree.XML(''' diff --git a/common/lib/xmodule/xmodule/tests/test_self_assessment.py b/common/lib/xmodule/xmodule/tests/test_self_assessment.py index 78dbb082ac..617b2b142a 100644 --- a/common/lib/xmodule/xmodule/tests/test_self_assessment.py +++ b/common/lib/xmodule/xmodule/tests/test_self_assessment.py @@ -46,11 +46,13 @@ class SelfAssessmentTest(unittest.TestCase): 'max_score': 1, 'display_name': "Name", 'accept_file_upload': False, + 'close_date': None } self.module = SelfAssessmentModule(test_system, self.location, self.definition, self.descriptor, - static_data, state, metadata=self.metadata) + static_data, + state, metadata=self.metadata) def test_get_html(self): html = self.module.get_html(test_system) diff --git a/common/lib/xmodule/xmodule/timeparse.py b/common/lib/xmodule/xmodule/timeparse.py index 1c3a780ad8..15a8233ccb 100644 --- a/common/lib/xmodule/xmodule/timeparse.py +++ b/common/lib/xmodule/xmodule/timeparse.py @@ -2,9 +2,12 @@ Helper functions for handling time in the format we like. """ import time +import re +from datetime import timedelta TIME_FORMAT = "%Y-%m-%dT%H:%M" +TIMEDELTA_REGEX = re.compile(r'^((?P\d+?) day(?:s?))?(\s)?((?P\d+?) hour(?:s?))?(\s)?((?P\d+?) minute(?:s)?)?(\s)?((?P\d+?) second(?:s)?)?$') def parse_time(time_str): """ @@ -22,3 +25,23 @@ def stringify_time(time_struct): Convert a time struct to a string """ return time.strftime(TIME_FORMAT, time_struct) + +def parse_timedelta(time_str): + """ + time_str: A string with the following components: + day[s] (optional) + hour[s] (optional) + minute[s] (optional) + 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)