From 74866a38b00c2ce5a593dc509dcb23f22febaaf0 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 29 May 2013 12:55:11 -0400 Subject: [PATCH 001/173] Move parseActions and statics out of evaluator() --- common/lib/calc/calc.py | 144 ++++++++++++++++++++-------------------- 1 file changed, 73 insertions(+), 71 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 2ee82e2fb4..0ab02e413b 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -37,16 +37,33 @@ default_variables = {'j': numpy.complex(0, 1), 'q': scipy.constants.e } + +ops = {"^": operator.pow, + "*": operator.mul, + "/": operator.truediv, + "+": operator.add, + "-": operator.sub, +} +# We eliminated extreme ones, since they're rarely used, and potentially +# confusing. They may also conflict with variables if we ever allow e.g. +# 5R instead of 5*R +suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9, + 'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24, + 'c': 1e-2, 'm': 1e-3, 'u': 1e-6, + 'n': 1e-9, 'p': 1e-12} # ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24} + log = logging.getLogger("mitx.courseware.capa") class UndefinedVariable(Exception): - def raiseself(self): - ''' Helper so we can use inside of a lambda ''' - raise self + pass + # unused for now + # def raiseself(self): + # ''' Helper so we can use inside of a lambda ''' + # raise self -general_whitespace = re.compile('[^\w]+') +general_whitespace = re.compile('[^\\w]+') def check_variables(string, variables): @@ -65,13 +82,61 @@ def check_variables(string, variables): for v in possible_variables: if len(v) == 0: continue - if v[0] <= '9' and '0' <= 'v': # Skip things that begin with numbers + if v[0] <= '9' and '0' <= v: # Skip things that begin with numbers continue if v not in variables: bad_variables.append(v) if len(bad_variables) > 0: raise UndefinedVariable(' '.join(bad_variables)) +def lower_dict(d): + return dict([(k.lower(), d[k]) for k in d]) + +def super_float(text): + ''' Like float, but with si extensions. 1k goes to 1000''' + if text[-1] in suffixes: + return float(text[:-1]) * suffixes[text[-1]] + else: + return float(text) + +def number_parse_action(x): # [ '7' ] -> [ 7 ] + return [super_float("".join(x))] + +def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512 + x = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^ + x.reverse() + x = reduce(lambda a, b: b ** a, x) + return x + +def parallel(x): # Parallel resistors [ 1 2 ] => 2/3 + # convert from pyparsing.ParseResults, which doesn't support '0 in x' + x = list(x) + if len(x) == 1: + return x[0] + if 0 in x: + return float('nan') + x = [1. / e for e in x if isinstance(e, numbers.Number)] # Ignore || + return 1. / sum(x) + +def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 + total = 0.0 + op = ops['+'] + for e in x: + if e in set('+-'): + op = ops[e] + else: + total = op(total, e) + return total + +def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66 + prod = 1.0 + op = ops['*'] + for e in x: + if e in set('*/'): + op = ops[e] + else: + prod = op(prod, e) + return prod def evaluator(variables, functions, string, cs=False): ''' @@ -86,12 +151,12 @@ def evaluator(variables, functions, string, cs=False): # log.debug("functions: {0}".format(functions)) # log.debug("string: {0}".format(string)) - def lower_dict(d): - return dict([(k.lower(), d[k]) for k in d]) - all_variables = copy.copy(default_variables) all_functions = copy.copy(default_functions) + def func_parse_action(x): + return [all_functions[x[0]](x[1])] + if not cs: all_variables = lower_dict(all_variables) all_functions = lower_dict(all_functions) @@ -113,69 +178,6 @@ def evaluator(variables, functions, string, cs=False): if string.strip() == "": return float('nan') - ops = {"^": operator.pow, - "*": operator.mul, - "/": operator.truediv, - "+": operator.add, - "-": operator.sub, - } - # We eliminated extreme ones, since they're rarely used, and potentially - # confusing. They may also conflict with variables if we ever allow e.g. - # 5R instead of 5*R - suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9, - 'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24, - 'c': 1e-2, 'm': 1e-3, 'u': 1e-6, - 'n': 1e-9, 'p': 1e-12} # ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24} - - def super_float(text): - ''' Like float, but with si extensions. 1k goes to 1000''' - if text[-1] in suffixes: - return float(text[:-1]) * suffixes[text[-1]] - else: - return float(text) - - def number_parse_action(x): # [ '7' ] -> [ 7 ] - return [super_float("".join(x))] - - def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512 - x = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^ - x.reverse() - x = reduce(lambda a, b: b ** a, x) - return x - - def parallel(x): # Parallel resistors [ 1 2 ] => 2/3 - # convert from pyparsing.ParseResults, which doesn't support '0 in x' - x = list(x) - if len(x) == 1: - return x[0] - if 0 in x: - return float('nan') - x = [1. / e for e in x if isinstance(e, numbers.Number)] # Ignore || - return 1. / sum(x) - - def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 - total = 0.0 - op = ops['+'] - for e in x: - if e in set('+-'): - op = ops[e] - else: - total = op(total, e) - return total - - def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66 - prod = 1.0 - op = ops['*'] - for e in x: - if e in set('*/'): - op = ops[e] - else: - prod = op(prod, e) - return prod - - def func_parse_action(x): - return [all_functions[x[0]](x[1])] - # SI suffixes and percent number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch()) (dot, minus, plus, times, div, lpar, rpar, exp) = map(Literal, ".-+*/()^") From ed45c505a39cf3a8aa094ee6c64591da1c604773 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 29 May 2013 12:55:51 -0400 Subject: [PATCH 002/173] Simpler pyparsing --- common/lib/calc/calc.py | 48 +++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 0ab02e413b..64053d6ca5 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -8,11 +8,11 @@ import numpy import numbers import scipy.constants -from pyparsing import Word, alphas, nums, oneOf, Literal -from pyparsing import ZeroOrMore, OneOrMore, StringStart -from pyparsing import StringEnd, Optional, Forward -from pyparsing import CaselessLiteral, Group, StringEnd -from pyparsing import NoMatch, stringEnd, alphanums +from pyparsing import Word, nums, Literal +from pyparsing import ZeroOrMore, MatchFirst +from pyparsing import Optional, Forward +from pyparsing import CaselessLiteral +from pyparsing import NoMatch, stringEnd, Suppress, Combine default_functions = {'sin': numpy.sin, 'cos': numpy.cos, @@ -179,17 +179,19 @@ def evaluator(variables, functions, string, cs=False): return float('nan') # SI suffixes and percent - number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch()) - (dot, minus, plus, times, div, lpar, rpar, exp) = map(Literal, ".-+*/()^") + number_suffix = MatchFirst([Literal(k) for k in suffixes.keys()]) + plus_minus = Literal('+') | Literal('-') + times_div = Literal('*') | Literal('/') number_part = Word(nums) # 0.33 or 7 or .34 or 16. inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part) + inner_number = Combine(inner_number) # by default pyparsing allows spaces between tokens--this prevents that # 0.33k or -17 - number = (Optional(minus | plus) + inner_number - + Optional(CaselessLiteral("E") + Optional((plus | minus)) + number_part) + number = (inner_number + + Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part) + Optional(number_suffix)) number = number.setParseAction(number_parse_action) # Convert to number @@ -197,40 +199,34 @@ def evaluator(variables, functions, string, cs=False): expr = Forward() factor = Forward() - def sreduce(f, l): - ''' Same as reduce, but handle len 1 and len 0 lists sensibly ''' - if len(l) == 0: - return NoMatch() - if len(l) == 1: - return l[0] - return reduce(f, l) - # Handle variables passed in. E.g. if we have {'R':0.5}, we make the substitution. # Special case for no variables because of how we understand PyParsing is put together if len(all_variables) > 0: # We sort the list so that var names (like "e2") match before # mathematical constants (like "e"). This is kind of a hack. all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True) - varnames = sreduce(lambda x, y: x | y, map(lambda x: CasedLiteral(x), all_variables_keys)) - varnames.setParseAction(lambda x: map(lambda y: all_variables[y], x)) + literal_all_vars = [CasedLiteral(k) for k in all_variables_keys] + varnames = MatchFirst(literal_all_vars) + varnames.setParseAction(lambda x: [all_variables[k] for k in x]) else: varnames = NoMatch() # Same thing for functions. if len(all_functions) > 0: - funcnames = sreduce(lambda x, y: x | y, - map(lambda x: CasedLiteral(x), all_functions.keys())) - function = funcnames + lpar.suppress() + expr + rpar.suppress() + funcnames = MatchFirst([CasedLiteral(k) for k in all_functions.keys()]) + function = funcnames + Suppress("(") + expr + Suppress(")") function.setParseAction(func_parse_action) else: function = NoMatch() - atom = number | function | varnames | lpar + expr + rpar - factor << (atom + ZeroOrMore(exp + atom)).setParseAction(exp_parse_action) # 7^6 + atom = number | function | varnames | Suppress("(") + expr + Suppress(")") + + # Do the following in the correct order to preserve order of operation + factor << (atom + ZeroOrMore("^" + atom)).setParseAction(exp_parse_action) # 7^6 paritem = factor + ZeroOrMore(Literal('||') + factor) # 5k || 4k paritem = paritem.setParseAction(parallel) - term = paritem + ZeroOrMore((times | div) + paritem) # 7 * 5 / 4 - 3 + term = paritem + ZeroOrMore(times_div + paritem) # 7 * 5 / 4 - 3 term = term.setParseAction(prod_parse_action) - expr << Optional((plus | minus)) + term + ZeroOrMore((plus | minus) + term) # -5 + 4 - 3 + expr << Optional(plus_minus) + term + ZeroOrMore(plus_minus + term) # -5 + 4 - 3 expr = expr.setParseAction(sum_parse_action) return (expr + stringEnd).parseString(string)[0] From 72d149caae1c5cd3909b59e850d94cb8ffc95c59 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 29 May 2013 13:25:48 -0400 Subject: [PATCH 003/173] Add docstrings and comments --- common/lib/calc/calc.py | 81 +++++++++++++++++++++++---- common/lib/capa/capa/responsetypes.py | 1 + 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 64053d6ca5..5d0aeb3fd1 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -1,3 +1,9 @@ +""" +Parser and evaluator for FormulaResponse and NumericalResponse + +Uses pyparsing to parse. Main function as of now is evaluator(). +""" + import copy import logging import math @@ -56,6 +62,10 @@ log = logging.getLogger("mitx.courseware.capa") class UndefinedVariable(Exception): + """ + Used to indicate the student input of a variable, which was unused by the + instructor. + """ pass # unused for now # def raiseself(self): @@ -67,7 +77,8 @@ general_whitespace = re.compile('[^\\w]+') def check_variables(string, variables): - '''Confirm the only variables in string are defined. + """ + Confirm the only variables in string are defined. Pyparsing uses a left-to-right parser, which makes the more elegant approach pretty hopeless. @@ -76,7 +87,7 @@ def check_variables(string, variables): undefined_variable = achar + Word(alphanums) undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself()) varnames = varnames | undefined_variable - ''' + """ possible_variables = re.split(general_whitespace, string) # List of all alnums in string bad_variables = list() for v in possible_variables: @@ -90,26 +101,59 @@ def check_variables(string, variables): raise UndefinedVariable(' '.join(bad_variables)) def lower_dict(d): + """ + takes each key in the dict and makes it lowercase, still mapping to the + same value. + + keep in mind that it is possible (but not useful?) to define different + variables that have the same lowercase representation. It would be hard to + tell which is used in the final dict and which isn't. + """ return dict([(k.lower(), d[k]) for k in d]) +# The following few functions define parse actions, which are run on lists of +# results from each parse component. They convert the strings and (previously +# calculated) numbers into the number that component represents. + def super_float(text): - ''' Like float, but with si extensions. 1k goes to 1000''' + """ + Like float, but with si extensions. 1k goes to 1000 + """ if text[-1] in suffixes: return float(text[:-1]) * suffixes[text[-1]] else: return float(text) -def number_parse_action(x): # [ '7' ] -> [ 7 ] +def number_parse_action(x): + """ + Create a float out of its string parts + + e.g. [ '7', '.', '13' ] -> [ 7.13 ] + Calls super_float above + """ return [super_float("".join(x))] -def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512 +def exp_parse_action(x): + """ + Take a list of numbers and exponentiate them, right to left + + e.g. [ 3, 2, 3 ] (which is 3^2^3 = 3^(2^3)) -> 6561 + """ x = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^ x.reverse() x = reduce(lambda a, b: b ** a, x) return x -def parallel(x): # Parallel resistors [ 1 2 ] => 2/3 - # convert from pyparsing.ParseResults, which doesn't support '0 in x' +def parallel(x): + """ + Compute numbers according to the parallel resistors operator + + BTW it is commutative. Its formula is given by + out = 1 / (1/in1 + 1/in2 + ...) + e.g. [ 1, 2 ] => 2/3 + + Return NaN if there is a zero among the inputs + """ x = list(x) if len(x) == 1: return x[0] @@ -119,6 +163,13 @@ def parallel(x): # Parallel resistors [ 1 2 ] => 2/3 return 1. / sum(x) def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 + """ + Add the inputs + + [ 1, '+', 2, '-', 3 ] -> 0 + + Allow a leading + or - + """ total = 0.0 op = ops['+'] for e in x: @@ -129,6 +180,11 @@ def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 return total def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66 + """ + Multiply the inputs + + [ 1, '*', 2, '/', 3 ] => 0.66 + """ prod = 1.0 op = ops['*'] for e in x: @@ -139,14 +195,13 @@ def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66 return prod def evaluator(variables, functions, string, cs=False): - ''' + """ Evaluate an expression. Variables are passed as a dictionary from string to value. Unary functions are passed as a dictionary from string to function. Variables must be floats. cs: Case sensitive - TODO: Fix it so we can pass integers and complex numbers in variables dict - ''' + """ # log.debug("variables: {0}".format(variables)) # log.debug("functions: {0}".format(functions)) # log.debug("string: {0}".format(string)) @@ -187,7 +242,8 @@ def evaluator(variables, functions, string, cs=False): # 0.33 or 7 or .34 or 16. inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part) - inner_number = Combine(inner_number) # by default pyparsing allows spaces between tokens--this prevents that + # by default pyparsing allows spaces between tokens--Combine prevents that + inner_number = Combine(inner_number) # 0.33k or -17 number = (inner_number @@ -209,6 +265,8 @@ def evaluator(variables, functions, string, cs=False): varnames = MatchFirst(literal_all_vars) varnames.setParseAction(lambda x: [all_variables[k] for k in x]) else: + # all_variables includes DEFAULT_VARIABLES, which isn't empty + # this is unreachable. Get rid of it? varnames = NoMatch() # Same thing for functions. @@ -217,6 +275,7 @@ def evaluator(variables, functions, string, cs=False): function = funcnames + Suppress("(") + expr + Suppress(")") function.setParseAction(func_parse_action) else: + # see note above (this is unreachable) function = NoMatch() atom = number | function | varnames | Suppress("(") + expr + Suppress(")") diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 0fa50079de..314d01e7e8 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1717,6 +1717,7 @@ class FormulaResponse(LoncapaResponse): student_variables = dict() # ranges give numerical ranges for testing for var in ranges: + # TODO: allow specified ranges (i.e. integers and complex numbers) for random variables value = random.uniform(*ranges[var]) instructor_variables[str(var)] = value student_variables[str(var)] = value From 862bb3f8bc34ff14618d92f91c5cbb9dbf458928 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 4 Jun 2013 11:34:52 -0400 Subject: [PATCH 004/173] Added the beginnings of the navigation tests I still need to refactor the methods but at this point, all tests work --- .../courseware/features/navigation.feature | 27 ++ .../courseware/features/navigation.py | 242 ++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 lms/djangoapps/courseware/features/navigation.feature create mode 100644 lms/djangoapps/courseware/features/navigation.py diff --git a/lms/djangoapps/courseware/features/navigation.feature b/lms/djangoapps/courseware/features/navigation.feature new file mode 100644 index 0000000000..f9cee87c89 --- /dev/null +++ b/lms/djangoapps/courseware/features/navigation.feature @@ -0,0 +1,27 @@ +Feature: Navigate Course + As a student in an edX course + In order to view the course properly + I want to be able to navigate through the content + + Scenario: I can navigate to a section + Given I am viewing a course with multiple sections + When I click on section "2" + Then I see the content of section "2" + + + Scenario: I can navigate to subsections + Given I am viewing a section with multiple subsections + When I click on subsection "2" + Then I see the content of subsection "2" + + Scenario: I can navigate to sequences + Given I am viewing a section with multiple sequences + When I click on sequence "2" + Then I see the content of sequence "2" + + Scenario: I can go back to where I was after I log out and back in + Given I am viewing a course with multiple sections + When I click on section "2" + And I visit the homepage + And I go to the section + Then I should see "You were most recently in Test Section2" somewhere on the page diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py new file mode 100644 index 0000000000..2f7f19f39a --- /dev/null +++ b/lms/djangoapps/courseware/features/navigation.py @@ -0,0 +1,242 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + +from lettuce import world, step +from django.contrib.auth.models import User +from lettuce.django import django_url +from student.models import CourseEnrollment +from common import course_id +from xmodule.modulestore import Location +from problems_setup import PROBLEM_DICT + +TEST_COURSE_ORG = 'edx' +TEST_COURSE_NAME = 'Test Course' +TEST_SECTION_NAME = 'Test Section' +SUBSECTION_2_LOC = None + + +@step(u'I am viewing a course with multiple sections') +def view_course_multiple_sections(step): + # First clear the modulestore so we don't try to recreate + # the same course twice + # This also ensures that the necessary templates are loaded + world.clear_courses() + + # Create the course + # We always use the same org and display name, + # but vary the course identifier (e.g. 600x or 191x) + course = world.CourseFactory.create(org=TEST_COURSE_ORG, + number="model_course", + display_name=TEST_COURSE_NAME) + + # Add a section to the course to contain problems + section1 = world.ItemFactory.create(parent_location=course.location, + display_name=TEST_SECTION_NAME+"1") + + # Add a section to the course to contain problems + section2 = world.ItemFactory.create(parent_location=course.location, + display_name=TEST_SECTION_NAME+"2") + + world.ItemFactory.create(parent_location=section1.location, + template='i4x://edx/templates/sequential/Empty', + display_name=TEST_SECTION_NAME+"1") + + world.ItemFactory.create(parent_location=section2.location, + template='i4x://edx/templates/sequential/Empty', + display_name=TEST_SECTION_NAME+"2") + + add_problem_to_course_section('model_course', 'multiple choice', section=1) + add_problem_to_course_section('model_course', 'drop down', section=2) + + # Create the user + world.create_user('robot') + u = User.objects.get(username='robot') + + # If the user is not already enrolled, enroll the user. + # TODO: change to factory + CourseEnrollment.objects.get_or_create(user=u, course_id=course_id("model_course")) + + world.log_in('robot', 'test') + chapter_name = (TEST_SECTION_NAME+"1").replace(" ", "_") + section_name = chapter_name + url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % + (chapter_name, section_name)) + + world.browser.visit(url) + + +@step(u'I am viewing a section with multiple subsections') +def view_course_multiple_subsections(step): + # First clear the modulestore so we don't try to recreate + # the same course twice + # This also ensures that the necessary templates are loaded + world.clear_courses() + + # Create the course + # We always use the same org and display name, + # but vary the course identifier (e.g. 600x or 191x) + course = world.CourseFactory.create(org=TEST_COURSE_ORG, + number="model_course", + display_name=TEST_COURSE_NAME) + + # Add a section to the course to contain problems + section1 = world.ItemFactory.create(parent_location=course.location, + display_name=TEST_SECTION_NAME+"1") + + world.ItemFactory.create(parent_location=section1.location, + template='i4x://edx/templates/sequential/Empty', + display_name=TEST_SECTION_NAME+"1") + + section2 = world.ItemFactory.create(parent_location=section1.location, + display_name=TEST_SECTION_NAME+"2") + + global SUBSECTION_2_LOC + SUBSECTION_2_LOC = section2.location + + + add_problem_to_course_section('model_course', 'multiple choice', section=1) + add_problem_to_course_section('model_course', 'drop down', section=1, subsection=2) + + # Create the user + world.create_user('robot') + u = User.objects.get(username='robot') + + # If the user is not already enrolled, enroll the user. + # TODO: change to factory + CourseEnrollment.objects.get_or_create(user=u, course_id=course_id("model_course")) + + world.log_in('robot', 'test') + chapter_name = (TEST_SECTION_NAME+"1").replace(" ", "_") + section_name = chapter_name + url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % + (chapter_name, section_name)) + + world.browser.visit(url) + + +@step(u'I am viewing a section with multiple sequences') +def view_course_multiple_sequences(step): + # First clear the modulestore so we don't try to recreate + # the same course twice + # This also ensures that the necessary templates are loaded + world.clear_courses() + + # Create the course + # We always use the same org and display name, + # but vary the course identifier (e.g. 600x or 191x) + course = world.CourseFactory.create(org=TEST_COURSE_ORG, + number="model_course", + display_name=TEST_COURSE_NAME) + + # Add a section to the course to contain problems + section1 = world.ItemFactory.create(parent_location=course.location, + display_name=TEST_SECTION_NAME+"1") + + + world.ItemFactory.create(parent_location=section1.location, + template='i4x://edx/templates/sequential/Empty', + display_name=TEST_SECTION_NAME+"1") + + add_problem_to_course_section('model_course', 'multiple choice', section=1) + add_problem_to_course_section('model_course', 'drop down', section=1) + + # Create the user + world.create_user('robot') + u = User.objects.get(username='robot') + + # If the user is not already enrolled, enroll the user. + # TODO: change to factory + CourseEnrollment.objects.get_or_create(user=u, course_id=course_id("model_course")) + + world.log_in('robot', 'test') + chapter_name = (TEST_SECTION_NAME+"1").replace(" ", "_") + section_name = chapter_name + url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % + (chapter_name, section_name)) + + world.browser.visit(url) + + +@step(u'I click on section "([^"]*)"') +def click_on_section(step, section): + section_css = 'h3[tabindex="-1"]' + elist = world.css_find(section_css) + assert not elist.is_empty() + elist.click() + subid = "ui-accordion-accordion-panel-"+str(int(section)-1) + subsection_css = 'ul[id="%s"]>li[class=" "] a' % subid + elist = world.css_find(subsection_css) + assert not elist.is_empty() + elist.click() + + +@step(u'I click on subsection "([^"]*)"') +def click_on_subsection(step, subsection): + subsection_css = 'ul[id="ui-accordion-accordion-panel-0"]>li[class=" "] a' + elist = world.css_find(subsection_css) + assert not elist.is_empty() + elist.click() + +@step(u'I click on sequence "([^"]*)"') +def click_on_subsection(step, sequence): + sequence_css = 'a[data-element="%s"]' % sequence + elist = world.css_find(sequence_css) + assert not elist.is_empty() + elist.click() + + +@step(u'I see the content of (?:sub)?section "([^"]*)"') +def see_section_content(step, section): + if section == "2": + text = 'The correct answer is Option 2' + elif section == "1": + text = 'The correct answer is Choice 3' + step.given('I should see "' + text + '" somewhere on the page') + + +@step(u'I see the content of sequence "([^"]*)"') +def see_sequence_content(step, sequence): + step.given('I see the content of section "2"') + + +@step(u'I go to the section') +def return_to_course(step): + world.click_link("View Course") + world.click_link("Courseware") + +### +#HELPERS +### + + +def add_problem_to_course_section(course, problem_type, extraMeta=None, section=1, subsection=1): + ''' + Add a problem to the course we have created using factories. + ''' + + assert(problem_type in PROBLEM_DICT) + + # Generate the problem XML using capa.tests.response_xml_factory + factory_dict = PROBLEM_DICT[problem_type] + problem_xml = factory_dict['factory'].build_xml(**factory_dict['kwargs']) + metadata = {'rerandomize': 'always'} if not 'metadata' in factory_dict else factory_dict['metadata'] + if extraMeta: + metadata = dict(metadata, **extraMeta) + + # Create a problem item using our generated XML + # We set rerandomize=always in the metadata so that the "Reset" button + # will appear. + template_name = "i4x://edx/templates/problem/Blank_Common_Problem" + world.ItemFactory.create(parent_location=section_location(course, section) if subsection == 1 else SUBSECTION_2_LOC, + template=template_name, + display_name=str(problem_type), + data=problem_xml, + metadata=metadata) + + +def section_location(course_num, section_num): + return Location(loc_or_tag="i4x", + org=TEST_COURSE_ORG, + course=course_num, + category='sequential', + name=(TEST_SECTION_NAME+str(section_num)).replace(" ", "_")) From c62cc23bc23967307b86f7f4ae5d060db35cbe3d Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 4 Jun 2013 13:06:18 -0400 Subject: [PATCH 005/173] Refactored Navigation Methods --- .../courseware/features/navigation.feature | 9 +- .../courseware/features/navigation.py | 196 +++++++----------- 2 files changed, 75 insertions(+), 130 deletions(-) diff --git a/lms/djangoapps/courseware/features/navigation.feature b/lms/djangoapps/courseware/features/navigation.feature index f9cee87c89..182a8ad4a9 100644 --- a/lms/djangoapps/courseware/features/navigation.feature +++ b/lms/djangoapps/courseware/features/navigation.feature @@ -6,22 +6,21 @@ Feature: Navigate Course Scenario: I can navigate to a section Given I am viewing a course with multiple sections When I click on section "2" - Then I see the content of section "2" + Then I should see the content of section "2" Scenario: I can navigate to subsections Given I am viewing a section with multiple subsections When I click on subsection "2" - Then I see the content of subsection "2" + Then I should see the content of subsection "2" Scenario: I can navigate to sequences Given I am viewing a section with multiple sequences When I click on sequence "2" - Then I see the content of sequence "2" + Then I should see the content of sequence "2" Scenario: I can go back to where I was after I log out and back in Given I am viewing a course with multiple sections When I click on section "2" - And I visit the homepage - And I go to the section + And I return later Then I should see "You were most recently in Test Section2" somewhere on the page diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py index 2f7f19f39a..06271a3002 100644 --- a/lms/djangoapps/courseware/features/navigation.py +++ b/lms/djangoapps/courseware/features/navigation.py @@ -13,28 +13,18 @@ TEST_COURSE_ORG = 'edx' TEST_COURSE_NAME = 'Test Course' TEST_SECTION_NAME = 'Test Section' SUBSECTION_2_LOC = None +COURSE_LOC = None @step(u'I am viewing a course with multiple sections') def view_course_multiple_sections(step): - # First clear the modulestore so we don't try to recreate - # the same course twice - # This also ensures that the necessary templates are loaded - world.clear_courses() - - # Create the course - # We always use the same org and display name, - # but vary the course identifier (e.g. 600x or 191x) - course = world.CourseFactory.create(org=TEST_COURSE_ORG, - number="model_course", - display_name=TEST_COURSE_NAME) - + create_course() # Add a section to the course to contain problems - section1 = world.ItemFactory.create(parent_location=course.location, + section1 = world.ItemFactory.create(parent_location=COURSE_LOC, display_name=TEST_SECTION_NAME+"1") # Add a section to the course to contain problems - section2 = world.ItemFactory.create(parent_location=course.location, + section2 = world.ItemFactory.create(parent_location=COURSE_LOC, display_name=TEST_SECTION_NAME+"2") world.ItemFactory.create(parent_location=section1.location, @@ -48,39 +38,15 @@ def view_course_multiple_sections(step): add_problem_to_course_section('model_course', 'multiple choice', section=1) add_problem_to_course_section('model_course', 'drop down', section=2) - # Create the user - world.create_user('robot') - u = User.objects.get(username='robot') - - # If the user is not already enrolled, enroll the user. - # TODO: change to factory - CourseEnrollment.objects.get_or_create(user=u, course_id=course_id("model_course")) - - world.log_in('robot', 'test') - chapter_name = (TEST_SECTION_NAME+"1").replace(" ", "_") - section_name = chapter_name - url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % - (chapter_name, section_name)) - - world.browser.visit(url) + create_user_and_visit_course() @step(u'I am viewing a section with multiple subsections') def view_course_multiple_subsections(step): - # First clear the modulestore so we don't try to recreate - # the same course twice - # This also ensures that the necessary templates are loaded - world.clear_courses() - - # Create the course - # We always use the same org and display name, - # but vary the course identifier (e.g. 600x or 191x) - course = world.CourseFactory.create(org=TEST_COURSE_ORG, - number="model_course", - display_name=TEST_COURSE_NAME) + create_course() # Add a section to the course to contain problems - section1 = world.ItemFactory.create(parent_location=course.location, + section1 = world.ItemFactory.create(parent_location=COURSE_LOC, display_name=TEST_SECTION_NAME+"1") world.ItemFactory.create(parent_location=section1.location, @@ -93,43 +59,17 @@ def view_course_multiple_subsections(step): global SUBSECTION_2_LOC SUBSECTION_2_LOC = section2.location - add_problem_to_course_section('model_course', 'multiple choice', section=1) add_problem_to_course_section('model_course', 'drop down', section=1, subsection=2) - # Create the user - world.create_user('robot') - u = User.objects.get(username='robot') - - # If the user is not already enrolled, enroll the user. - # TODO: change to factory - CourseEnrollment.objects.get_or_create(user=u, course_id=course_id("model_course")) - - world.log_in('robot', 'test') - chapter_name = (TEST_SECTION_NAME+"1").replace(" ", "_") - section_name = chapter_name - url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % - (chapter_name, section_name)) - - world.browser.visit(url) + create_user_and_visit_course() @step(u'I am viewing a section with multiple sequences') def view_course_multiple_sequences(step): - # First clear the modulestore so we don't try to recreate - # the same course twice - # This also ensures that the necessary templates are loaded - world.clear_courses() - - # Create the course - # We always use the same org and display name, - # but vary the course identifier (e.g. 600x or 191x) - course = world.CourseFactory.create(org=TEST_COURSE_ORG, - number="model_course", - display_name=TEST_COURSE_NAME) - + create_course() # Add a section to the course to contain problems - section1 = world.ItemFactory.create(parent_location=course.location, + section1 = world.ItemFactory.create(parent_location=COURSE_LOC, display_name=TEST_SECTION_NAME+"1") @@ -140,12 +80,70 @@ def view_course_multiple_sequences(step): add_problem_to_course_section('model_course', 'multiple choice', section=1) add_problem_to_course_section('model_course', 'drop down', section=1) - # Create the user + create_user_and_visit_course() + + +@step(u'I click on section "([^"]*)"') +def click_on_section(step, section): + section_css = 'h3[tabindex="-1"]' + world.css_click(section_css) + + subid = "ui-accordion-accordion-panel-"+str(int(section)-1) + subsection_css = 'ul[id="%s"]>li[class=" "] a' % subid + world.css_click(subsection_css) + + +@step(u'I click on subsection "([^"]*)"') +def click_on_subsection(step, subsection): + subsection_css = 'ul[id="ui-accordion-accordion-panel-0"]>li[class=" "]>a' + world.css_click(subsection_css) + + +@step(u'I click on sequence "([^"]*)"') +def click_on_sequence(step, sequence): + sequence_css = 'a[data-element="%s"]' % sequence + world.css_click(sequence_css) + + +@step(u'I should see the content of (?:sub)?section "([^"]*)"') +def see_section_content(step, section): + if section == "2": + text = 'The correct answer is Option 2' + elif section == "1": + text = 'The correct answer is Choice 3' + step.given('I should see "' + text + '" somewhere on the page') + + +@step(u'I should see the content of sequence "([^"]*)"') +def see_sequence_content(step, sequence): + step.given('I should see the content of section "2"') + + +@step(u'I return later') +def return_to_course(step): + step.given('I visit the homepage') + world.click_link("View Course") + world.click_link("Courseware") + +##################### +# HELPERS +##################### + + +def create_course(): + world.clear_courses() + + course = world.CourseFactory.create(org=TEST_COURSE_ORG, + number="model_course", + display_name=TEST_COURSE_NAME) + global COURSE_LOC + COURSE_LOC = course.location + + +def create_user_and_visit_course(): world.create_user('robot') u = User.objects.get(username='robot') - # If the user is not already enrolled, enroll the user. - # TODO: change to factory CourseEnrollment.objects.get_or_create(user=u, course_id=course_id("model_course")) world.log_in('robot', 'test') @@ -157,58 +155,6 @@ def view_course_multiple_sequences(step): world.browser.visit(url) -@step(u'I click on section "([^"]*)"') -def click_on_section(step, section): - section_css = 'h3[tabindex="-1"]' - elist = world.css_find(section_css) - assert not elist.is_empty() - elist.click() - subid = "ui-accordion-accordion-panel-"+str(int(section)-1) - subsection_css = 'ul[id="%s"]>li[class=" "] a' % subid - elist = world.css_find(subsection_css) - assert not elist.is_empty() - elist.click() - - -@step(u'I click on subsection "([^"]*)"') -def click_on_subsection(step, subsection): - subsection_css = 'ul[id="ui-accordion-accordion-panel-0"]>li[class=" "] a' - elist = world.css_find(subsection_css) - assert not elist.is_empty() - elist.click() - -@step(u'I click on sequence "([^"]*)"') -def click_on_subsection(step, sequence): - sequence_css = 'a[data-element="%s"]' % sequence - elist = world.css_find(sequence_css) - assert not elist.is_empty() - elist.click() - - -@step(u'I see the content of (?:sub)?section "([^"]*)"') -def see_section_content(step, section): - if section == "2": - text = 'The correct answer is Option 2' - elif section == "1": - text = 'The correct answer is Choice 3' - step.given('I should see "' + text + '" somewhere on the page') - - -@step(u'I see the content of sequence "([^"]*)"') -def see_sequence_content(step, sequence): - step.given('I see the content of section "2"') - - -@step(u'I go to the section') -def return_to_course(step): - world.click_link("View Course") - world.click_link("Courseware") - -### -#HELPERS -### - - def add_problem_to_course_section(course, problem_type, extraMeta=None, section=1, subsection=1): ''' Add a problem to the course we have created using factories. From a85a7f71df6c0bc889b2d5cbe40926b3663d375e Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 29 May 2013 13:34:58 -0400 Subject: [PATCH 006/173] Rename variables; get rid of OPS --- common/lib/calc/calc.py | 170 ++++++++++++++++++++-------------------- 1 file changed, 87 insertions(+), 83 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 5d0aeb3fd1..f862b41542 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -11,16 +11,15 @@ import operator import re import numpy -import numbers import scipy.constants -from pyparsing import Word, nums, Literal -from pyparsing import ZeroOrMore, MatchFirst -from pyparsing import Optional, Forward -from pyparsing import CaselessLiteral -from pyparsing import NoMatch, stringEnd, Suppress, Combine +from pyparsing import (Word, nums, Literal, + ZeroOrMore, MatchFirst, + Optional, Forward, + CaselessLiteral, + NoMatch, stringEnd, Suppress, Combine) -default_functions = {'sin': numpy.sin, +DEFAULT_FUNCTIONS = {'sin': numpy.sin, 'cos': numpy.cos, 'tan': numpy.tan, 'sqrt': numpy.sqrt, @@ -34,7 +33,7 @@ default_functions = {'sin': numpy.sin, 'fact': math.factorial, 'factorial': math.factorial } -default_variables = {'j': numpy.complex(0, 1), +DEFAULT_VARIABLES = {'j': numpy.complex(0, 1), 'e': numpy.e, 'pi': numpy.pi, 'k': scipy.constants.k, @@ -43,22 +42,15 @@ default_variables = {'j': numpy.complex(0, 1), 'q': scipy.constants.e } - -ops = {"^": operator.pow, - "*": operator.mul, - "/": operator.truediv, - "+": operator.add, - "-": operator.sub, -} # We eliminated extreme ones, since they're rarely used, and potentially # confusing. They may also conflict with variables if we ever allow e.g. # 5R instead of 5*R -suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9, +SUFFIXES = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9, 'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24, 'c': 1e-2, 'm': 1e-3, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12} # ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24} -log = logging.getLogger("mitx.courseware.capa") +LOG = logging.getLogger("mitx.courseware.capa") class UndefinedVariable(Exception): @@ -73,13 +65,12 @@ class UndefinedVariable(Exception): # raise self -general_whitespace = re.compile('[^\\w]+') - - def check_variables(string, variables): """ Confirm the only variables in string are defined. + Otherwise, raise an UndefinedVariable containing all bad variables. + Pyparsing uses a left-to-right parser, which makes the more elegant approach pretty hopeless. @@ -88,19 +79,22 @@ def check_variables(string, variables): undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself()) varnames = varnames | undefined_variable """ - possible_variables = re.split(general_whitespace, string) # List of all alnums in string + general_whitespace = re.compile('[^\\w]+') + # List of all alnums in string + possible_variables = re.split(general_whitespace, string) bad_variables = list() - for v in possible_variables: - if len(v) == 0: + for var in possible_variables: + if len(var) == 0: continue - if v[0] <= '9' and '0' <= v: # Skip things that begin with numbers + if var[0] <= '9' and '0' <= var: # Skip things that begin with numbers continue - if v not in variables: - bad_variables.append(v) + if var not in variables: + bad_variables.append(var) if len(bad_variables) > 0: raise UndefinedVariable(' '.join(bad_variables)) -def lower_dict(d): + +def lower_dict(input_dict): """ takes each key in the dict and makes it lowercase, still mapping to the same value. @@ -109,7 +103,8 @@ def lower_dict(d): variables that have the same lowercase representation. It would be hard to tell which is used in the final dict and which isn't. """ - return dict([(k.lower(), d[k]) for k in d]) + return dict([(k.lower(), input_dict[k]) for k in input_dict]) + # The following few functions define parse actions, which are run on lists of # results from each parse component. They convert the strings and (previously @@ -119,32 +114,37 @@ def super_float(text): """ Like float, but with si extensions. 1k goes to 1000 """ - if text[-1] in suffixes: - return float(text[:-1]) * suffixes[text[-1]] + if text[-1] in SUFFIXES: + return float(text[:-1]) * SUFFIXES[text[-1]] else: return float(text) -def number_parse_action(x): + +def number_parse_action(parse_result): """ Create a float out of its string parts e.g. [ '7', '.', '13' ] -> [ 7.13 ] Calls super_float above """ - return [super_float("".join(x))] + return super_float("".join(parse_result)) -def exp_parse_action(x): + +def exp_parse_action(parse_result): """ Take a list of numbers and exponentiate them, right to left e.g. [ 3, 2, 3 ] (which is 3^2^3 = 3^(2^3)) -> 6561 """ - x = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^ - x.reverse() - x = reduce(lambda a, b: b ** a, x) - return x + # pyparsing.ParseResults doesn't play well with reverse() + parse_result = parse_result.asList() + parse_result.reverse() + # the result of an exponentiation is called a power + power = reduce(lambda a, b: b ** a, parse_result) + return power -def parallel(x): + +def parallel(parse_result): """ Compute numbers according to the parallel resistors operator @@ -154,15 +154,17 @@ def parallel(x): Return NaN if there is a zero among the inputs """ - x = list(x) - if len(x) == 1: - return x[0] - if 0 in x: + # convert from pyparsing.ParseResults, which doesn't support '0 in parse_result' + parse_result = parse_result.asList() + if len(parse_result) == 1: + return parse_result[0] + if 0 in parse_result: return float('nan') - x = [1. / e for e in x if isinstance(e, numbers.Number)] # Ignore || - return 1. / sum(x) + reciprocals = [1. / e for e in parse_result] + return 1. / sum(reciprocals) -def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 + +def sum_parse_action(parse_result): """ Add the inputs @@ -171,29 +173,35 @@ def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 Allow a leading + or - """ total = 0.0 - op = ops['+'] - for e in x: - if e in set('+-'): - op = ops[e] + current_op = operator.add + for token in parse_result: + if token is '+': + current_op = operator.add + elif token is '-': + current_op = operator.sub else: - total = op(total, e) + total = current_op(total, token) return total -def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66 + +def prod_parse_action(parse_result): """ Multiply the inputs [ 1, '*', 2, '/', 3 ] => 0.66 """ prod = 1.0 - op = ops['*'] - for e in x: - if e in set('*/'): - op = ops[e] + current_op = operator.mul + for token in parse_result: + if token is '*': + current_op = operator.mul + elif token is '/': + current_op = operator.truediv else: - prod = op(prod, e) + prod = current_op(prod, token) return prod + def evaluator(variables, functions, string, cs=False): """ Evaluate an expression. Variables are passed as a dictionary @@ -202,20 +210,12 @@ def evaluator(variables, functions, string, cs=False): cs: Case sensitive """ - # log.debug("variables: {0}".format(variables)) - # log.debug("functions: {0}".format(functions)) - # log.debug("string: {0}".format(string)) - - all_variables = copy.copy(default_variables) - all_functions = copy.copy(default_functions) - - def func_parse_action(x): - return [all_functions[x[0]](x[1])] - - if not cs: - all_variables = lower_dict(all_variables) - all_functions = lower_dict(all_functions) + # LOG.debug("variables: {0}".format(variables)) + # LOG.debug("functions: {0}".format(functions)) + # LOG.debug("string: {0}".format(string)) + all_variables = copy.copy(DEFAULT_VARIABLES) + all_functions = copy.copy(DEFAULT_FUNCTIONS) all_variables.update(variables) all_functions.update(functions) @@ -234,7 +234,7 @@ def evaluator(variables, functions, string, cs=False): return float('nan') # SI suffixes and percent - number_suffix = MatchFirst([Literal(k) for k in suffixes.keys()]) + number_suffix = MatchFirst([Literal(k) for k in SUFFIXES.keys()]) plus_minus = Literal('+') | Literal('-') times_div = Literal('*') | Literal('/') @@ -249,11 +249,10 @@ def evaluator(variables, functions, string, cs=False): number = (inner_number + Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part) + Optional(number_suffix)) - number = number.setParseAction(number_parse_action) # Convert to number + number.setParseAction(number_parse_action) # Convert to number # Predefine recursive variables expr = Forward() - factor = Forward() # Handle variables passed in. E.g. if we have {'R':0.5}, we make the substitution. # Special case for no variables because of how we understand PyParsing is put together @@ -261,9 +260,10 @@ def evaluator(variables, functions, string, cs=False): # We sort the list so that var names (like "e2") match before # mathematical constants (like "e"). This is kind of a hack. all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True) - literal_all_vars = [CasedLiteral(k) for k in all_variables_keys] - varnames = MatchFirst(literal_all_vars) - varnames.setParseAction(lambda x: [all_variables[k] for k in x]) + varnames = MatchFirst([CasedLiteral(k) for k in all_variables_keys]) + varnames.setParseAction( + lambda x: [all_variables[k] for k in x] + ) else: # all_variables includes DEFAULT_VARIABLES, which isn't empty # this is unreachable. Get rid of it? @@ -273,7 +273,9 @@ def evaluator(variables, functions, string, cs=False): if len(all_functions) > 0: funcnames = MatchFirst([CasedLiteral(k) for k in all_functions.keys()]) function = funcnames + Suppress("(") + expr + Suppress(")") - function.setParseAction(func_parse_action) + function.setParseAction( + lambda x: [all_functions[x[0]](x[1])] + ) else: # see note above (this is unreachable) function = NoMatch() @@ -281,11 +283,13 @@ def evaluator(variables, functions, string, cs=False): atom = number | function | varnames | Suppress("(") + expr + Suppress(")") # Do the following in the correct order to preserve order of operation - factor << (atom + ZeroOrMore("^" + atom)).setParseAction(exp_parse_action) # 7^6 - paritem = factor + ZeroOrMore(Literal('||') + factor) # 5k || 4k - paritem = paritem.setParseAction(parallel) - term = paritem + ZeroOrMore(times_div + paritem) # 7 * 5 / 4 - 3 - term = term.setParseAction(prod_parse_action) - expr << Optional(plus_minus) + term + ZeroOrMore(plus_minus + term) # -5 + 4 - 3 - expr = expr.setParseAction(sum_parse_action) + pow_term = atom + ZeroOrMore(Suppress("^") + atom) + pow_term.setParseAction(exp_parse_action) # 7^6 + par_term = pow_term + ZeroOrMore(Suppress('||') + pow_term) # 5k || 4k + par_term.setParseAction(parallel) + prod_term = par_term + ZeroOrMore(times_div + par_term) # 7 * 5 / 4 - 3 + prod_term.setParseAction(prod_parse_action) + sum_term = Optional(plus_minus) + prod_term + ZeroOrMore(plus_minus + prod_term) # -5 + 4 - 3 + sum_term.setParseAction(sum_parse_action) + expr << sum_term # finish the recursion return (expr + stringEnd).parseString(string)[0] From 83f1f9c2fc78442c77376457094ba674bca59c49 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 5 Jun 2013 15:50:35 -0400 Subject: [PATCH 007/173] Set numpy so it does not print out warnings on student input --- common/lib/calc/calc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index f862b41542..cc3a883221 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -13,6 +13,10 @@ import re import numpy import scipy.constants +# have numpy raise errors on functions outside its domain +# See http://docs.scipy.org/doc/numpy/reference/generated/numpy.seterr.html +numpy.seterr(all='ignore') # Also: 'ignore', 'warn' (default), 'raise' + from pyparsing import (Word, nums, Literal, ZeroOrMore, MatchFirst, Optional, Forward, From 9a631fe47654e6c220d02fa7ac9b7dcdace9c48b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 29 May 2013 17:36:40 -0400 Subject: [PATCH 008/173] All uses of safe_exec need to get the correct random seed. --- common/lib/capa/capa/responsetypes.py | 32 +++++++- .../lib/capa/capa/tests/test_responsetypes.py | 77 ++++++++++++++++--- 2 files changed, 96 insertions(+), 13 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 0fa50079de..a13ed3ca11 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -288,7 +288,13 @@ class LoncapaResponse(object): } try: - safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path'], slug=self.id) + safe_exec.safe_exec( + code, + globals_dict, + python_path=self.context['python_path'], + slug=self.id, + random_seed=self.context['seed'], + ) except Exception as err: msg = 'Error %s in evaluating hint function %s' % (err, hintfn) msg += "\nSee XML source line %s" % getattr( @@ -973,7 +979,13 @@ class CustomResponse(LoncapaResponse): 'ans': ans, } globals_dict.update(kwargs) - safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path'], slug=self.id) + safe_exec.safe_exec( + code, + globals_dict, + python_path=self.context['python_path'], + slug=self.id, + random_seed=self.context['seed'], + ) return globals_dict['cfn_return'] return check_function @@ -1090,7 +1102,13 @@ class CustomResponse(LoncapaResponse): # exec the check function if isinstance(self.code, basestring): try: - safe_exec.safe_exec(self.code, self.context, cache=self.system.cache, slug=self.id) + safe_exec.safe_exec( + self.code, + self.context, + cache=self.system.cache, + slug=self.id, + random_seed=self.context['seed'], + ) except Exception as err: self._handle_exec_exception(err) @@ -1814,7 +1832,13 @@ class SchematicResponse(LoncapaResponse): ] self.context.update({'submission': submission}) try: - safe_exec.safe_exec(self.code, self.context, cache=self.system.cache, slug=self.id) + safe_exec.safe_exec( + self.code, + self.context, + cache=self.system.cache, + slug=self.id, + random_seed=self.context['seed'], + ) except Exception as err: msg = 'Error %s in evaluating SchematicResponse' % err raise ResponseError(msg) diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 780c475b09..20de19f567 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -640,6 +640,23 @@ class StringResponseTest(ResponseTest): correct_map = problem.grade_answers(input_dict) self.assertEquals(correct_map.get_hint('1_2_1'), "Hello??") + def test_hint_function_randomization(self): + # The hint function should get the seed from the problem. + problem = self.build_problem( + answer="1", + hintfn="gimme_a_random_hint", + script=textwrap.dedent(""" + def gimme_a_random_hint(answer_ids, student_answers, new_cmap, old_cmap): + answer = str(random.randint(0, 1e9)) + new_cmap.set_hint_and_mode(answer_ids[0], answer, "always") + + """) + ) + correct_map = problem.grade_answers({'1_2_1': '2'}) + hint = correct_map.get_hint('1_2_1') + r = random.Random(problem.seed) + self.assertEqual(hint, str(r.randint(0, 1e9))) + class CodeResponseTest(ResponseTest): from response_xml_factory import CodeResponseXMLFactory @@ -948,7 +965,6 @@ class CustomResponseTest(ResponseTest): xml_factory_class = CustomResponseXMLFactory def test_inline_code(self): - # For inline code, we directly modify global context variables # 'answers' is a list of answers provided to us # 'correct' is a list we fill in with True/False @@ -961,15 +977,14 @@ class CustomResponseTest(ResponseTest): self.assert_grade(problem, '0', 'incorrect') def test_inline_message(self): - # Inline code can update the global messages list # to pass messages to the CorrectMap for a particular input # The code can also set the global overall_message (str) # to pass a message that applies to the whole response inline_script = textwrap.dedent(""" - messages[0] = "Test Message" - overall_message = "Overall message" - """) + messages[0] = "Test Message" + overall_message = "Overall message" + """) problem = self.build_problem(answer=inline_script) input_dict = {'1_2_1': '0'} @@ -983,8 +998,19 @@ class CustomResponseTest(ResponseTest): overall_msg = correctmap.get_overall_message() self.assertEqual(overall_msg, "Overall message") - def test_function_code_single_input(self): + def test_inline_randomization(self): + # Make sure the seed from the problem gets fed into the script execution. + inline_script = """messages[0] = str(random.randint(0, 1e9))""" + problem = self.build_problem(answer=inline_script) + input_dict = {'1_2_1': '0'} + correctmap = problem.grade_answers(input_dict) + + input_msg = correctmap.get_msg('1_2_1') + r = random.Random(problem.seed) + self.assertEqual(input_msg, str(r.randint(0, 1e9))) + + def test_function_code_single_input(self): # For function code, we pass in these arguments: # # 'expect' is the expect attribute of the @@ -1212,6 +1238,29 @@ class CustomResponseTest(ResponseTest): with self.assertRaises(ResponseError): problem.grade_answers({'1_2_1': '42'}) + def test_setup_randomization(self): + # Ensure that the problem setup script gets the random seed from the problem. + script = textwrap.dedent(""" + num = random.randint(0, 1e9) + """) + problem = self.build_problem(script=script) + r = random.Random(problem.seed) + self.assertEqual(r.randint(0, 1e9), problem.context['num']) + + def test_check_function_randomization(self): + # The check function should get random-seeded from the problem. + script = textwrap.dedent(""" + def check_func(expect, answer_given): + return {'ok': True, 'msg': str(random.randint(0, 1e9))} + """) + + problem = self.build_problem(script=script, cfn="check_func", expect="42") + input_dict = {'1_2_1': '42'} + correct_map = problem.grade_answers(input_dict) + msg = correct_map.get_msg('1_2_1') + r = random.Random(problem.seed) + self.assertEqual(msg, str(r.randint(0, 1e9))) + def test_module_imports_inline(self): ''' Check that the correct modules are available to custom @@ -1275,7 +1324,6 @@ class SchematicResponseTest(ResponseTest): xml_factory_class = SchematicResponseXMLFactory def test_grade(self): - # Most of the schematic-specific work is handled elsewhere # (in client-side JavaScript) # The is responsible only for executing the @@ -1290,7 +1338,7 @@ class SchematicResponseTest(ResponseTest): # The actual dictionary would contain schematic information # sent from the JavaScript simulation - submission_dict = {'test': 'test'} + submission_dict = {'test': 'the_answer'} input_dict = {'1_2_1': json.dumps(submission_dict)} correct_map = problem.grade_answers(input_dict) @@ -1299,8 +1347,19 @@ class SchematicResponseTest(ResponseTest): # is what we expect) self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct') - def test_script_exception(self): + def test_check_function_randomization(self): + # The check function should get a random seed from the problem. + script = "correct = ['correct' if (submission[0]['num'] == random.randint(0, 1e9)) else 'incorrect']" + problem = self.build_problem(answer=script) + r = random.Random(problem.seed) + submission_dict = {'num': r.randint(0, 1e9)} + input_dict = {'1_2_1': json.dumps(submission_dict)} + correct_map = problem.grade_answers(input_dict) + + self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct') + + def test_script_exception(self): # Construct a script that will raise an exception script = "raise Exception('test')" problem = self.build_problem(answer=script) From cab49716b56bd1d23e57ffb98805b60cdbfe65f3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 6 Jun 2013 14:14:30 -0400 Subject: [PATCH 009/173] Whitelisted courses now run Python code outside the sandbox. --- common/lib/capa/capa/capa_problem.py | 1 + common/lib/capa/capa/responsetypes.py | 4 ++++ common/lib/capa/capa/safe_exec/safe_exec.py | 13 +++++++++-- .../capa/safe_exec/tests/test_safe_exec.py | 22 +++++++++++++++++++ requirements/edx/github.txt | 2 +- 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 150b3b3c9b..7dcd7b925e 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -470,6 +470,7 @@ class LoncapaProblem(object): python_path=python_path, cache=self.system.cache, slug=self.problem_id, + unsafely=self.system.can_execute_unsafe_code(), ) except Exception as err: log.exception("Error while execing script code: " + all_code) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index a13ed3ca11..6183ca2ade 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -294,6 +294,7 @@ class LoncapaResponse(object): python_path=self.context['python_path'], slug=self.id, random_seed=self.context['seed'], + unsafely=self.system.can_execute_unsafe_code(), ) except Exception as err: msg = 'Error %s in evaluating hint function %s' % (err, hintfn) @@ -985,6 +986,7 @@ class CustomResponse(LoncapaResponse): python_path=self.context['python_path'], slug=self.id, random_seed=self.context['seed'], + unsafely=self.system.can_execute_unsafe_code(), ) return globals_dict['cfn_return'] return check_function @@ -1108,6 +1110,7 @@ class CustomResponse(LoncapaResponse): cache=self.system.cache, slug=self.id, random_seed=self.context['seed'], + unsafely=self.system.can_execute_unsafe_code(), ) except Exception as err: self._handle_exec_exception(err) @@ -1838,6 +1841,7 @@ class SchematicResponse(LoncapaResponse): cache=self.system.cache, slug=self.id, random_seed=self.context['seed'], + unsafely=self.system.can_execute_unsafe_code(), ) except Exception as err: msg = 'Error %s in evaluating SchematicResponse' % err diff --git a/common/lib/capa/capa/safe_exec/safe_exec.py b/common/lib/capa/capa/safe_exec/safe_exec.py index 67e93be46f..3ab8f0bf9e 100644 --- a/common/lib/capa/capa/safe_exec/safe_exec.py +++ b/common/lib/capa/capa/safe_exec/safe_exec.py @@ -1,6 +1,7 @@ """Capa's specialized use of codejail.safe_exec.""" from codejail.safe_exec import safe_exec as codejail_safe_exec +from codejail.safe_exec import not_safe_exec as codejail_not_safe_exec from codejail.safe_exec import json_safe, SafeExecException from . import lazymod from statsd import statsd @@ -71,7 +72,7 @@ def update_hash(hasher, obj): @statsd.timed('capa.safe_exec.time') -def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None, slug=None): +def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None, slug=None, unsafely=False): """ Execute python code safely. @@ -90,6 +91,8 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None `slug` is an arbitrary string, a description that's meaningful to the caller, that will be used in log messages. + If `unsafely` is true, then the code will actually be executed without sandboxing. + """ # Check the cache for a previous result. if cache: @@ -111,9 +114,15 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None # Create the complete code we'll run. code_prolog = CODE_PROLOG % random_seed + # Decide which code executor to use. + if unsafely: + exec_fn = codejail_not_safe_exec + else: + exec_fn = codejail_safe_exec + # Run the code! Results are side effects in globals_dict. try: - codejail_safe_exec( + exec_fn( code_prolog + LAZY_IMPORTS + code, globals_dict, python_path=python_path, slug=slug, ) diff --git a/common/lib/capa/capa/safe_exec/tests/test_safe_exec.py b/common/lib/capa/capa/safe_exec/tests/test_safe_exec.py index 4592af8305..f8a8a32297 100644 --- a/common/lib/capa/capa/safe_exec/tests/test_safe_exec.py +++ b/common/lib/capa/capa/safe_exec/tests/test_safe_exec.py @@ -1,13 +1,17 @@ """Test safe_exec.py""" import hashlib +import os import os.path import random import textwrap import unittest +from nose.plugins.skip import SkipTest + from capa.safe_exec import safe_exec, update_hash from codejail.safe_exec import SafeExecException +from codejail.jail_code import is_configured class TestSafeExec(unittest.TestCase): @@ -68,6 +72,24 @@ class TestSafeExec(unittest.TestCase): self.assertIn("ZeroDivisionError", cm.exception.message) +class TestSafeOrNot(unittest.TestCase): + def test_cant_do_something_forbidden(self): + # Can't test for forbiddenness if CodeJail isn't configured for python. + if not is_configured("python"): + raise SkipTest + + g = {} + with self.assertRaises(SafeExecException) as cm: + safe_exec("import os; files = os.listdir('/')", g) + self.assertIn("OSError", cm.exception.message) + self.assertIn("Permission denied", cm.exception.message) + + def test_can_do_something_forbidden_if_run_unsafely(self): + g = {} + safe_exec("import os; files = os.listdir('/')", g, unsafely=True) + self.assertEqual(g['files'], os.listdir('/')) + + class DictCache(object): """A cache implementation over a simple dict, for testing.""" diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index fc9070bba3..8b5ab8df48 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -9,5 +9,5 @@ # Our libraries: -e git+https://github.com/edx/XBlock.git@2144a25d#egg=XBlock --e git+https://github.com/edx/codejail.git@5fb5fa0#egg=codejail +-e git+https://github.com/edx/codejail.git@0a1b468#egg=codejail -e git+https://github.com/edx/diff-cover.git@v0.1.0#egg=diff_cover From 8d15b74a9751a2d42a0e4662effb3b2b3bbc4f13 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 6 Jun 2013 14:38:59 -0400 Subject: [PATCH 010/173] Fixed errors in spacing and refactoring out of scenario --- .../courseware/features/navigation.feature | 3 +-- .../courseware/features/navigation.py | 27 ++++++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/lms/djangoapps/courseware/features/navigation.feature b/lms/djangoapps/courseware/features/navigation.feature index 182a8ad4a9..8fd8b54c1a 100644 --- a/lms/djangoapps/courseware/features/navigation.feature +++ b/lms/djangoapps/courseware/features/navigation.feature @@ -8,7 +8,6 @@ Feature: Navigate Course When I click on section "2" Then I should see the content of section "2" - Scenario: I can navigate to subsections Given I am viewing a section with multiple subsections When I click on subsection "2" @@ -23,4 +22,4 @@ Feature: Navigate Course Given I am viewing a course with multiple sections When I click on section "2" And I return later - Then I should see "You were most recently in Test Section2" somewhere on the page + Then I should see that I was most recently in section "2" diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py index 06271a3002..1f6308c6c5 100644 --- a/lms/djangoapps/courseware/features/navigation.py +++ b/lms/djangoapps/courseware/features/navigation.py @@ -21,19 +21,19 @@ def view_course_multiple_sections(step): create_course() # Add a section to the course to contain problems section1 = world.ItemFactory.create(parent_location=COURSE_LOC, - display_name=TEST_SECTION_NAME+"1") + display_name=section_name(1)) # Add a section to the course to contain problems section2 = world.ItemFactory.create(parent_location=COURSE_LOC, - display_name=TEST_SECTION_NAME+"2") + display_name=section_name(2)) world.ItemFactory.create(parent_location=section1.location, template='i4x://edx/templates/sequential/Empty', - display_name=TEST_SECTION_NAME+"1") + display_name=section_name(1)) world.ItemFactory.create(parent_location=section2.location, template='i4x://edx/templates/sequential/Empty', - display_name=TEST_SECTION_NAME+"2") + display_name=section_name(2)) add_problem_to_course_section('model_course', 'multiple choice', section=1) add_problem_to_course_section('model_course', 'drop down', section=2) @@ -47,14 +47,14 @@ def view_course_multiple_subsections(step): # Add a section to the course to contain problems section1 = world.ItemFactory.create(parent_location=COURSE_LOC, - display_name=TEST_SECTION_NAME+"1") + display_name=section_name(1)) world.ItemFactory.create(parent_location=section1.location, template='i4x://edx/templates/sequential/Empty', - display_name=TEST_SECTION_NAME+"1") + display_name=section_name(1)) section2 = world.ItemFactory.create(parent_location=section1.location, - display_name=TEST_SECTION_NAME+"2") + display_name=section_name(2)) global SUBSECTION_2_LOC SUBSECTION_2_LOC = section2.location @@ -70,12 +70,12 @@ def view_course_multiple_sequences(step): create_course() # Add a section to the course to contain problems section1 = world.ItemFactory.create(parent_location=COURSE_LOC, - display_name=TEST_SECTION_NAME+"1") + display_name=section_name(1)) world.ItemFactory.create(parent_location=section1.location, template='i4x://edx/templates/sequential/Empty', - display_name=TEST_SECTION_NAME+"1") + display_name=section_name(1)) add_problem_to_course_section('model_course', 'multiple choice', section=1) add_problem_to_course_section('model_course', 'drop down', section=1) @@ -125,11 +125,20 @@ def return_to_course(step): world.click_link("View Course") world.click_link("Courseware") + +@step(u'I should see that I was most recently in section "([^"]*)"') +def see_recent_section(step, section): + step.given('I should see "You were most recently in %s" somewhere on the page' % section_name(int(section))) + ##################### # HELPERS ##################### +def section_name(section): + return TEST_SECTION_NAME+str(section) + + def create_course(): world.clear_courses() From 1fefec2176d7af23b17a197a0ebb4115c3c53288 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 6 Jun 2013 15:28:45 -0400 Subject: [PATCH 011/173] Fixed the need of a global variable --- .../courseware/features/navigation.py | 67 +++++++------------ 1 file changed, 25 insertions(+), 42 deletions(-) diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py index 1f6308c6c5..8bf81c0ec5 100644 --- a/lms/djangoapps/courseware/features/navigation.py +++ b/lms/djangoapps/courseware/features/navigation.py @@ -5,38 +5,35 @@ from lettuce import world, step from django.contrib.auth.models import User from lettuce.django import django_url from student.models import CourseEnrollment -from common import course_id -from xmodule.modulestore import Location +from common import course_id, course_location from problems_setup import PROBLEM_DICT TEST_COURSE_ORG = 'edx' TEST_COURSE_NAME = 'Test Course' TEST_SECTION_NAME = 'Test Section' -SUBSECTION_2_LOC = None -COURSE_LOC = None @step(u'I am viewing a course with multiple sections') def view_course_multiple_sections(step): create_course() # Add a section to the course to contain problems - section1 = world.ItemFactory.create(parent_location=COURSE_LOC, + section1 = world.ItemFactory.create(parent_location=course_location('model_course'), display_name=section_name(1)) # Add a section to the course to contain problems - section2 = world.ItemFactory.create(parent_location=COURSE_LOC, + section2 = world.ItemFactory.create(parent_location=course_location('model_course'), display_name=section_name(2)) - world.ItemFactory.create(parent_location=section1.location, + place1 = world.ItemFactory.create(parent_location=section1.location, template='i4x://edx/templates/sequential/Empty', display_name=section_name(1)) - world.ItemFactory.create(parent_location=section2.location, + place2 = world.ItemFactory.create(parent_location=section2.location, template='i4x://edx/templates/sequential/Empty', display_name=section_name(2)) - add_problem_to_course_section('model_course', 'multiple choice', section=1) - add_problem_to_course_section('model_course', 'drop down', section=2) + add_problem_to_course_section('model_course', 'multiple choice', place1.location) + add_problem_to_course_section('model_course', 'drop down', place2.location) create_user_and_visit_course() @@ -46,21 +43,18 @@ def view_course_multiple_subsections(step): create_course() # Add a section to the course to contain problems - section1 = world.ItemFactory.create(parent_location=COURSE_LOC, + section1 = world.ItemFactory.create(parent_location=course_location('model_course'), display_name=section_name(1)) - world.ItemFactory.create(parent_location=section1.location, + place1 = world.ItemFactory.create(parent_location=section1.location, template='i4x://edx/templates/sequential/Empty', display_name=section_name(1)) - section2 = world.ItemFactory.create(parent_location=section1.location, + place2 = world.ItemFactory.create(parent_location=section1.location, display_name=section_name(2)) - global SUBSECTION_2_LOC - SUBSECTION_2_LOC = section2.location - - add_problem_to_course_section('model_course', 'multiple choice', section=1) - add_problem_to_course_section('model_course', 'drop down', section=1, subsection=2) + add_problem_to_course_section('model_course', 'multiple choice', place1.location) + add_problem_to_course_section('model_course', 'drop down', place2.location) create_user_and_visit_course() @@ -69,21 +63,20 @@ def view_course_multiple_subsections(step): def view_course_multiple_sequences(step): create_course() # Add a section to the course to contain problems - section1 = world.ItemFactory.create(parent_location=COURSE_LOC, + section1 = world.ItemFactory.create(parent_location=course_location('model_course'), display_name=section_name(1)) - - world.ItemFactory.create(parent_location=section1.location, + place1 = world.ItemFactory.create(parent_location=section1.location, template='i4x://edx/templates/sequential/Empty', display_name=section_name(1)) - add_problem_to_course_section('model_course', 'multiple choice', section=1) - add_problem_to_course_section('model_course', 'drop down', section=1) + add_problem_to_course_section('model_course', 'multiple choice', place1.location) + add_problem_to_course_section('model_course', 'drop down', place1.location) create_user_and_visit_course() -@step(u'I click on section "([^"]*)"') +@step(u'I click on section "([^"]*)"$') def click_on_section(step, section): section_css = 'h3[tabindex="-1"]' world.css_click(section_css) @@ -93,19 +86,19 @@ def click_on_section(step, section): world.css_click(subsection_css) -@step(u'I click on subsection "([^"]*)"') +@step(u'I click on subsection "([^"]*)"$') def click_on_subsection(step, subsection): subsection_css = 'ul[id="ui-accordion-accordion-panel-0"]>li[class=" "]>a' world.css_click(subsection_css) -@step(u'I click on sequence "([^"]*)"') +@step(u'I click on sequence "([^"]*)"$') def click_on_sequence(step, sequence): sequence_css = 'a[data-element="%s"]' % sequence world.css_click(sequence_css) -@step(u'I should see the content of (?:sub)?section "([^"]*)"') +@step(u'I should see the content of (?:sub)?section "([^"]*)"$') def see_section_content(step, section): if section == "2": text = 'The correct answer is Option 2' @@ -114,7 +107,7 @@ def see_section_content(step, section): step.given('I should see "' + text + '" somewhere on the page') -@step(u'I should see the content of sequence "([^"]*)"') +@step(u'I should see the content of sequence "([^"]*)"$') def see_sequence_content(step, sequence): step.given('I should see the content of section "2"') @@ -126,7 +119,7 @@ def return_to_course(step): world.click_link("Courseware") -@step(u'I should see that I was most recently in section "([^"]*)"') +@step(u'I should see that I was most recently in section "([^"]*)"$') def see_recent_section(step, section): step.given('I should see "You were most recently in %s" somewhere on the page' % section_name(int(section))) @@ -142,11 +135,9 @@ def section_name(section): def create_course(): world.clear_courses() - course = world.CourseFactory.create(org=TEST_COURSE_ORG, + world.CourseFactory.create(org=TEST_COURSE_ORG, number="model_course", display_name=TEST_COURSE_NAME) - global COURSE_LOC - COURSE_LOC = course.location def create_user_and_visit_course(): @@ -164,7 +155,7 @@ def create_user_and_visit_course(): world.browser.visit(url) -def add_problem_to_course_section(course, problem_type, extraMeta=None, section=1, subsection=1): +def add_problem_to_course_section(course, problem_type, parent_location, extraMeta=None): ''' Add a problem to the course we have created using factories. ''' @@ -182,16 +173,8 @@ def add_problem_to_course_section(course, problem_type, extraMeta=None, section= # We set rerandomize=always in the metadata so that the "Reset" button # will appear. template_name = "i4x://edx/templates/problem/Blank_Common_Problem" - world.ItemFactory.create(parent_location=section_location(course, section) if subsection == 1 else SUBSECTION_2_LOC, + world.ItemFactory.create(parent_location=parent_location, template=template_name, display_name=str(problem_type), data=problem_xml, metadata=metadata) - - -def section_location(course_num, section_num): - return Location(loc_or_tag="i4x", - org=TEST_COURSE_ORG, - course=course_num, - category='sequential', - name=(TEST_SECTION_NAME+str(section_num)).replace(" ", "_")) From b05ff885fad26d116b8755f7dd249038b6321389 Mon Sep 17 00:00:00 2001 From: Nate Hardison Date: Thu, 6 Jun 2013 14:18:08 -0700 Subject: [PATCH 012/173] Replace /faq route in LMS urls This route was mistakenly removed by the theming changes since the "FAQ" marketing link actually points to help_edx, not faq_edx. --- lms/urls.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lms/urls.py b/lms/urls.py index d2265463de..fc97c75a36 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -98,6 +98,8 @@ if not settings.MITX_FEATURES["USE_CUSTOM_THEME"]: url(r'^press$', 'student.views.press', name="press"), url(r'^media-kit$', 'static_template_view.views.render', {'template': 'media-kit.html'}, name="media-kit"), + url(r'^faq$', 'static_template_view.views.render', + {'template': 'faq.html'}, name="faq_edx"), url(r'^help$', 'static_template_view.views.render', {'template': 'help.html'}, name="help_edx"), @@ -125,7 +127,7 @@ for key, value in settings.MKTG_URL_LINK_MAP.items(): continue # These urls are enabled separately - if key == "ROOT" or key == "COURSES": + if key == "ROOT" or key == "COURSES" or key == "FAQ": continue # Make the assumptions that the templates are all in the same dir From 0baec0a164fda2881f043acf0d7f83dfe99e9a58 Mon Sep 17 00:00:00 2001 From: cahrens Date: Fri, 7 Jun 2013 15:45:34 -0400 Subject: [PATCH 013/173] Move string fields, get rid of hard-coded list of booleans. --- cms/xmodule_namespace.py | 2 - common/lib/xmodule/xmodule/capa_module.py | 16 +++--- .../xmodule/combined_open_ended_module.py | 20 +++---- common/lib/xmodule/xmodule/fields.py | 41 -------------- .../xmodule/xmodule/peer_grading_module.py | 23 ++++---- .../lib/xmodule/xmodule/tests/test_fields.py | 54 +------------------ .../xmodule/xmodule/tests/test_xml_module.py | 10 ++-- .../lib/xmodule/xmodule/word_cloud_module.py | 9 ++-- common/lib/xmodule/xmodule/xml_module.py | 35 +++++------- lms/xmodule_namespace.py | 8 +-- requirements/edx/github.txt | 2 +- 11 files changed, 56 insertions(+), 164 deletions(-) diff --git a/cms/xmodule_namespace.py b/cms/xmodule_namespace.py index 4857fe68ca..eef4b41f37 100644 --- a/cms/xmodule_namespace.py +++ b/cms/xmodule_namespace.py @@ -5,7 +5,6 @@ Namespace defining common fields used by Studio for all blocks import datetime from xblock.core import Namespace, Scope, ModelType, String -from xmodule.fields import StringyBoolean class DateTuple(ModelType): @@ -28,4 +27,3 @@ class CmsNamespace(Namespace): """ published_date = DateTuple(help="Date when the module was published", scope=Scope.settings) published_by = String(help="Id of the user who published this module", scope=Scope.settings) - diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 9ac540138e..38a0ea599a 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -18,8 +18,8 @@ from .progress import Progress from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor from xmodule.exceptions import NotFoundError, ProcessingError -from xblock.core import Scope, String, Boolean, Object -from .fields import Timedelta, Date, StringyInteger, StringyFloat +from xblock.core import Scope, String, Boolean, Object, Integer, Float +from .fields import Timedelta, Date from xmodule.util.date_utils import time_to_datetime log = logging.getLogger("mitx.courseware") @@ -65,8 +65,8 @@ class ComplexEncoder(json.JSONEncoder): class CapaFields(object): - attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state) - max_attempts = StringyInteger( + attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state) + max_attempts = Integer( display_name="Maximum Attempts", help="Defines the number of times a student can try to answer this problem. If the value is not set, infinite attempts are allowed.", values={"min": 1}, scope=Scope.settings @@ -99,8 +99,8 @@ class CapaFields(object): input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state) student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state) done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state) - seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state) - weight = StringyFloat( + seed = Integer(help="Random seed for this student", scope=Scope.user_state) + weight = Float( display_name="Problem Weight", help="Defines the number of points each problem is worth. If the value is not set, each response field in the problem is worth one point.", values={"min": 0, "step": .1}, @@ -315,7 +315,7 @@ class CapaModule(CapaFields, XModule): # If the user has forced the save button to display, # then show it as long as the problem is not closed # (past due / too many attempts) - if self.force_save_button == "true": + if self.force_save_button: return not self.closed() else: is_survey_question = (self.max_attempts == 0) @@ -782,7 +782,7 @@ class CapaModule(CapaFields, XModule): return {'success': msg} raise - self.attempts = self.attempts + 1 + self.attempts += 1 self.lcp.done = True self.set_state_from_lcp() diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index b3f0e19109..6c3725161a 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -5,10 +5,10 @@ from pkg_resources import resource_string from xmodule.raw_module import RawDescriptor from .x_module import XModule -from xblock.core import Integer, Scope, String, List +from xblock.core import Integer, Scope, String, List, Float, Boolean from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor from collections import namedtuple -from .fields import Date, StringyFloat, StringyInteger, StringyBoolean +from .fields import Date log = logging.getLogger("mitx.courseware") @@ -53,27 +53,27 @@ class CombinedOpenEndedFields(object): help="This name appears in the horizontal navigation at the top of the page.", default="Open Ended Grading", scope=Scope.settings ) - current_task_number = StringyInteger(help="Current task that the student is on.", default=0, scope=Scope.user_state) + current_task_number = Integer(help="Current task that the student is on.", default=0, scope=Scope.user_state) task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.user_state) state = String(help="Which step within the current task that the student is on.", default="initial", scope=Scope.user_state) - student_attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, + student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state) - ready_to_reset = StringyBoolean( + ready_to_reset = Boolean( help="If the problem is ready to be reset or not.", default=False, scope=Scope.user_state ) - attempts = StringyInteger( + attempts = Integer( display_name="Maximum Attempts", help="The number of times the student can try to answer this problem.", default=1, scope=Scope.settings, values = {"min" : 1 } ) - is_graded = StringyBoolean(display_name="Graded", help="Whether or not the problem is graded.", default=False, scope=Scope.settings) - accept_file_upload = StringyBoolean( + is_graded = Boolean(display_name="Graded", help="Whether or not the problem is graded.", default=False, scope=Scope.settings) + accept_file_upload = Boolean( display_name="Allow File Uploads", help="Whether or not the student can submit files as a response.", default=False, scope=Scope.settings ) - skip_spelling_checks = StringyBoolean( + skip_spelling_checks = Boolean( display_name="Disable Quality Filter", help="If False, the Quality Filter is enabled and submissions with poor spelling, short length, or poor grammar will not be peer reviewed.", default=False, scope=Scope.settings @@ -86,7 +86,7 @@ class CombinedOpenEndedFields(object): ) version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings) data = String(help="XML data for the problem", scope=Scope.content) - weight = StringyFloat( + weight = Float( display_name="Problem Weight", help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.", scope=Scope.settings, values = {"min" : 0 , "step": ".1"} diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py index 3d56b7941e..bb85714252 100644 --- a/common/lib/xmodule/xmodule/fields.py +++ b/common/lib/xmodule/xmodule/fields.py @@ -7,8 +7,6 @@ from xblock.core import ModelType import datetime import dateutil.parser -from xblock.core import Integer, Float, Boolean - log = logging.getLogger(__name__) @@ -83,42 +81,3 @@ class Timedelta(ModelType): if cur_value > 0: values.append("%d %s" % (cur_value, attr)) return ' '.join(values) - - -class StringyInteger(Integer): - """ - A model type that converts from strings to integers when reading from json. - If value does not parse as an int, returns None. - """ - def from_json(self, value): - try: - return int(value) - except: - return None - - -class StringyFloat(Float): - """ - A model type that converts from string to floats when reading from json. - If value does not parse as a float, returns None. - """ - def from_json(self, value): - try: - return float(value) - except: - return None - - -class StringyBoolean(Boolean): - """ - Reads strings from JSON as booleans. - - If the string is 'true' (case insensitive), then return True, - otherwise False. - - JSON values that aren't strings are returned as-is. - """ - def from_json(self, value): - if isinstance(value, basestring): - return value.lower() == 'true' - return value diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index ccc3e31f51..4dc553458b 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -10,8 +10,8 @@ from .x_module import XModule from xmodule.raw_module import RawDescriptor from xmodule.modulestore.django import modulestore from .timeinfo import TimeInfo -from xblock.core import Object, String, Scope -from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean +from xblock.core import Object, String, Scope, Boolean, Integer, Float +from xmodule.fields import Date from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService from open_ended_grading_classes import combined_open_ended_rubric @@ -20,7 +20,6 @@ log = logging.getLogger(__name__) USE_FOR_SINGLE_LOCATION = False LINK_TO_LOCATION = "" -TRUE_DICT = [True, "True", "true", "TRUE"] MAX_SCORE = 1 IS_GRADED = False @@ -28,7 +27,7 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please class PeerGradingFields(object): - use_for_single_location = StringyBoolean( + use_for_single_location = Boolean( display_name="Show Single Problem", help='When True, only the single problem specified by "Link to Problem Location" is shown. ' 'When False, a panel is displayed with all problems available for peer grading.', @@ -39,14 +38,14 @@ class PeerGradingFields(object): help='The location of the problem being graded. Only used when "Show Single Problem" is True.', default=LINK_TO_LOCATION, scope=Scope.settings ) - is_graded = StringyBoolean( + is_graded = Boolean( display_name="Graded", help='Defines whether the student gets credit for grading this problem. Only used when "Show Single Problem" is True.', default=IS_GRADED, scope=Scope.settings ) due_date = Date(help="Due date that should be displayed.", default=None, scope=Scope.settings) grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings) - max_grade = StringyInteger( + max_grade = Integer( help="The maximum grade that a student can receive for this problem.", default=MAX_SCORE, scope=Scope.settings, values={"min": 0} ) @@ -54,7 +53,7 @@ class PeerGradingFields(object): help="Student data for a given peer grading problem.", scope=Scope.user_state ) - weight = StringyFloat( + weight = Float( display_name="Problem Weight", help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.", scope=Scope.settings, values={"min": 0, "step": ".1"} @@ -84,7 +83,7 @@ class PeerGradingModule(PeerGradingFields, XModule): else: self.peer_gs = MockPeerGradingService() - if self.use_for_single_location in TRUE_DICT: + if self.use_for_single_location: try: self.linked_problem = modulestore().get_instance(self.system.course_id, self.link_to_location) except: @@ -146,7 +145,7 @@ class PeerGradingModule(PeerGradingFields, XModule): """ if self.closed(): return self.peer_grading_closed() - if self.use_for_single_location not in TRUE_DICT: + if not self.use_for_single_location: return self.peer_grading() else: return self.peer_grading_problem({'location': self.link_to_location})['html'] @@ -203,7 +202,7 @@ class PeerGradingModule(PeerGradingFields, XModule): 'score': score, 'total': max_score, } - if self.use_for_single_location not in TRUE_DICT or self.is_graded not in TRUE_DICT: + if not self.use_for_single_location or not self.is_graded: return score_dict try: @@ -238,7 +237,7 @@ class PeerGradingModule(PeerGradingFields, XModule): randomization, and 5/7 on another ''' max_grade = None - if self.use_for_single_location in TRUE_DICT and self.is_graded in TRUE_DICT: + if self.use_for_single_location and self.is_graded: max_grade = self.max_grade return max_grade @@ -556,7 +555,7 @@ class PeerGradingModule(PeerGradingFields, XModule): Show individual problem interface ''' if get is None or get.get('location') is None: - if self.use_for_single_location not in TRUE_DICT: + if not self.use_for_single_location: #This is an error case, because it must be set to use a single location to be called without get parameters #This is a dev_facing_error log.error( diff --git a/common/lib/xmodule/xmodule/tests/test_fields.py b/common/lib/xmodule/xmodule/tests/test_fields.py index 9642f7c595..e08508ac99 100644 --- a/common/lib/xmodule/xmodule/tests/test_fields.py +++ b/common/lib/xmodule/xmodule/tests/test_fields.py @@ -2,7 +2,7 @@ import datetime import unittest from django.utils.timezone import UTC -from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean +from xmodule.fields import Date import time class DateTest(unittest.TestCase): @@ -78,55 +78,3 @@ class DateTest(unittest.TestCase): DateTest.date.from_json("2012-12-31T23:00:01-01:00")), "2013-01-01T00:00:01Z") - -class StringyIntegerTest(unittest.TestCase): - def assertEquals(self, expected, arg): - self.assertEqual(expected, StringyInteger().from_json(arg)) - - def test_integer(self): - self.assertEquals(5, '5') - self.assertEquals(0, '0') - self.assertEquals(-1023, '-1023') - - def test_none(self): - self.assertEquals(None, None) - self.assertEquals(None, 'abc') - self.assertEquals(None, '[1]') - self.assertEquals(None, '1.023') - - -class StringyFloatTest(unittest.TestCase): - - def assertEquals(self, expected, arg): - self.assertEqual(expected, StringyFloat().from_json(arg)) - - def test_float(self): - self.assertEquals(.23, '.23') - self.assertEquals(5, '5') - self.assertEquals(0, '0.0') - self.assertEquals(-1023.22, '-1023.22') - - def test_none(self): - self.assertEquals(None, None) - self.assertEquals(None, 'abc') - self.assertEquals(None, '[1]') - - -class StringyBooleanTest(unittest.TestCase): - - def assertEquals(self, expected, arg): - self.assertEqual(expected, StringyBoolean().from_json(arg)) - - def test_false(self): - self.assertEquals(False, "false") - self.assertEquals(False, "False") - self.assertEquals(False, "") - self.assertEquals(False, "hahahahah") - - def test_true(self): - self.assertEquals(True, "true") - self.assertEquals(True, "TruE") - - def test_pass_through(self): - self.assertEquals(123, 123) - diff --git a/common/lib/xmodule/xmodule/tests/test_xml_module.py b/common/lib/xmodule/xmodule/tests/test_xml_module.py index dd59ca2b48..18cd11650f 100644 --- a/common/lib/xmodule/xmodule/tests/test_xml_module.py +++ b/common/lib/xmodule/xmodule/tests/test_xml_module.py @@ -2,8 +2,8 @@ #pylint: disable=C0111 from xmodule.x_module import XModuleFields -from xblock.core import Scope, String, Object, Boolean -from xmodule.fields import Date, StringyInteger, StringyFloat +from xblock.core import Scope, String, Object, Boolean, Integer, Float +from xmodule.fields import Date from xmodule.xml_module import XmlDescriptor import unittest from .import test_system @@ -17,7 +17,7 @@ class CrazyJsonString(String): class TestFields(object): # Will be returned by editable_metadata_fields. - max_attempts = StringyInteger(scope=Scope.settings, default=1000, values={'min': 1, 'max': 10}) + max_attempts = Integer(scope=Scope.settings, default=1000, values={'min': 1, 'max': 10}) # Will not be returned by editable_metadata_fields because filtered out by non_editable_metadata_fields. due = Date(scope=Scope.settings) # Will not be returned by editable_metadata_fields because is not Scope.settings. @@ -33,9 +33,9 @@ class TestFields(object): {'display_name': 'second', 'value': 'value b'}] ) # Used for testing select type - float_select = StringyFloat(scope=Scope.settings, default=.999, values=[1.23, 0.98]) + float_select = Float(scope=Scope.settings, default=.999, values=[1.23, 0.98]) # Used for testing float type - float_non_select = StringyFloat(scope=Scope.settings, default=.999, values={'min': 0, 'step': .3}) + float_non_select = Float(scope=Scope.settings, default=.999, values={'min': 0, 'step': .3}) # Used for testing that Booleans get mapped to select type boolean_select = Boolean(scope=Scope.settings) diff --git a/common/lib/xmodule/xmodule/word_cloud_module.py b/common/lib/xmodule/xmodule/word_cloud_module.py index e38b8cf195..6605f9b870 100644 --- a/common/lib/xmodule/xmodule/word_cloud_module.py +++ b/common/lib/xmodule/xmodule/word_cloud_module.py @@ -14,8 +14,7 @@ from xmodule.raw_module import RawDescriptor from xmodule.editing_module import MetadataOnlyEditingDescriptor from xmodule.x_module import XModule -from xblock.core import Scope, Object, Boolean, List -from fields import StringyBoolean, StringyInteger +from xblock.core import Scope, Object, Boolean, List, Integer log = logging.getLogger(__name__) @@ -32,21 +31,21 @@ def pretty_bool(value): class WordCloudFields(object): """XFields for word cloud.""" - num_inputs = StringyInteger( + num_inputs = Integer( display_name="Inputs", help="Number of text boxes available for students to input words/sentences.", scope=Scope.settings, default=5, values={"min": 1} ) - num_top_words = StringyInteger( + num_top_words = Integer( display_name="Maximum Words", help="Maximum number of words to be displayed in generated word cloud.", scope=Scope.settings, default=250, values={"min": 1} ) - display_student_percents = StringyBoolean( + display_student_percents = Boolean( display_name="Show Percents", help="Statistics are shown for entered words near that word.", scope=Scope.settings, diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index 2f54bbf405..56f8b6fd15 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -120,25 +120,15 @@ class XmlDescriptor(XModuleDescriptor): metadata_to_export_to_policy = ('discussion_topics') - # A dictionary mapping xml attribute names AttrMaps that describe how - # to import and export them - # Allow json to specify either the string "true", or the bool True. The string is preferred. - to_bool = lambda val: val == 'true' or val == True - from_bool = lambda val: str(val).lower() - bool_map = AttrMap(to_bool, from_bool) - - to_int = lambda val: int(val) - from_int = lambda val: str(val) - int_map = AttrMap(to_int, from_int) - xml_attribute_map = { - # type conversion: want True/False in python, "true"/"false" in xml - 'graded': bool_map, - 'hide_progress_tab': bool_map, - 'allow_anonymous': bool_map, - 'allow_anonymous_to_peers': bool_map, - 'show_timezone': bool_map, - } + @classmethod + def get_map_for_field(cls, attr): + for field in set(cls.fields + cls.lms.fields): + if field.name == attr: + from_xml = lambda val: field.deserialize(val) + to_xml = lambda val : field.serialize(val) + return AttrMap(from_xml, to_xml) + return AttrMap() @classmethod def definition_from_xml(cls, xml_object, system): @@ -188,7 +178,6 @@ class XmlDescriptor(XModuleDescriptor): filepath, location.url(), str(err)) raise Exception, msg, sys.exc_info()[2] - @classmethod def load_definition(cls, xml_object, system, location): '''Load a descriptor definition from the specified xml_object. @@ -246,7 +235,7 @@ class XmlDescriptor(XModuleDescriptor): # don't load these continue - attr_map = cls.xml_attribute_map.get(attr, AttrMap()) + attr_map = cls.get_map_for_field(attr) metadata[attr] = attr_map.from_xml(val) return metadata @@ -258,7 +247,7 @@ class XmlDescriptor(XModuleDescriptor): through the attrmap. Updates the metadata dict in place. """ for attr in policy: - attr_map = cls.xml_attribute_map.get(attr, AttrMap()) + attr_map = cls.get_map_for_field(attr) metadata[cls._translate(attr)] = attr_map.from_xml(policy[attr]) @classmethod @@ -347,7 +336,7 @@ class XmlDescriptor(XModuleDescriptor): def export_to_xml(self, resource_fs): """ - Returns an xml string representign this module, and all modules + Returns an xml string representing this module, and all modules underneath it. May also write required resources out to resource_fs Assumes that modules have single parentage (that no module appears twice @@ -372,7 +361,7 @@ class XmlDescriptor(XModuleDescriptor): """Get the value for this attribute that we want to store. (Possible format conversion through an AttrMap). """ - attr_map = self.xml_attribute_map.get(attr, AttrMap()) + attr_map = self.get_map_for_field(attr) return attr_map.to_xml(self._model_data[attr]) # Add the non-inherited metadata diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py index 6b78d18db0..aaef0b76db 100644 --- a/lms/xmodule_namespace.py +++ b/lms/xmodule_namespace.py @@ -1,15 +1,15 @@ """ Namespace that defines fields common to all blocks used in the LMS """ -from xblock.core import Namespace, Boolean, Scope, String -from xmodule.fields import Date, Timedelta, StringyFloat, StringyBoolean +from xblock.core import Namespace, Boolean, Scope, String, Float +from xmodule.fields import Date, Timedelta class LmsNamespace(Namespace): """ Namespace that defines fields common to all blocks used in the LMS """ - hide_from_toc = StringyBoolean( + hide_from_toc = Boolean( help="Whether to display this module in the table of contents", default=False, scope=Scope.settings @@ -37,7 +37,7 @@ class LmsNamespace(Namespace): ) showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed") rerandomize = String(help="When to rerandomize the problem", default="always", scope=Scope.settings) - days_early_for_beta = StringyFloat( + days_early_for_beta = Float( help="Number of days early to show content to beta users", default=None, scope=Scope.settings diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index fc9070bba3..fb20fd2b22 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -8,6 +8,6 @@ -e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk # Our libraries: --e git+https://github.com/edx/XBlock.git@2144a25d#egg=XBlock +-e git+https://github.com/edx/XBlock.git@a56a79d8#egg=XBlock -e git+https://github.com/edx/codejail.git@5fb5fa0#egg=codejail -e git+https://github.com/edx/diff-cover.git@v0.1.0#egg=diff_cover From f3b92312d920a60c3d74b91b20355a3c8b3dd11d Mon Sep 17 00:00:00 2001 From: cahrens Date: Fri, 7 Jun 2013 15:58:31 -0400 Subject: [PATCH 014/173] Add test for serialize/deserialize. --- common/lib/xmodule/xmodule/tests/test_fields.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/common/lib/xmodule/xmodule/tests/test_fields.py b/common/lib/xmodule/xmodule/tests/test_fields.py index e08508ac99..a5730d55b3 100644 --- a/common/lib/xmodule/xmodule/tests/test_fields.py +++ b/common/lib/xmodule/xmodule/tests/test_fields.py @@ -78,3 +78,18 @@ class DateTest(unittest.TestCase): DateTest.date.from_json("2012-12-31T23:00:01-01:00")), "2013-01-01T00:00:01Z") + def test_serialize(self): + self.assertEqual( + DateTest.date.serialize("2012-12-31T23:59:59Z"), + '"2012-12-31T23:59:59Z"' + ) + + def test_deserialize(self): + self.assertEqual( + '2012-12-31T23:59:59Z', + DateTest.date.deserialize("2012-12-31T23:59:59Z"), + ) + self.assertEqual( + '2012-12-31T23:59:59Z', + DateTest.date.deserialize('"2012-12-31T23:59:59Z"'), + ) From 1273bc22b389b657dbc7b8e2fdd4278af6946491 Mon Sep 17 00:00:00 2001 From: cahrens Date: Fri, 7 Jun 2013 17:24:07 -0400 Subject: [PATCH 015/173] Additional test coverage. --- common/lib/xmodule/xmodule/capa_module.py | 2 +- .../lib/xmodule/xmodule/tests/test_fields.py | 39 ++++++++++++++++++- common/test/data/full/course.xml | 2 +- requirements/edx/github.txt | 2 +- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 38a0ea599a..392d29134b 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -782,7 +782,7 @@ class CapaModule(CapaFields, XModule): return {'success': msg} raise - self.attempts += 1 + self.attempts = self.attempts + 1 self.lcp.done = True self.set_state_from_lcp() diff --git a/common/lib/xmodule/xmodule/tests/test_fields.py b/common/lib/xmodule/xmodule/tests/test_fields.py index a5730d55b3..d944566e97 100644 --- a/common/lib/xmodule/xmodule/tests/test_fields.py +++ b/common/lib/xmodule/xmodule/tests/test_fields.py @@ -2,7 +2,7 @@ import datetime import unittest from django.utils.timezone import UTC -from xmodule.fields import Date +from xmodule.fields import Date, Timedelta import time class DateTest(unittest.TestCase): @@ -93,3 +93,40 @@ class DateTest(unittest.TestCase): '2012-12-31T23:59:59Z', DateTest.date.deserialize('"2012-12-31T23:59:59Z"'), ) + + +class TimedeltaTest(unittest.TestCase): + delta = Timedelta() + + def test_from_json(self): + self.assertEqual( + TimedeltaTest.delta.from_json('1 day 12 hours 59 minutes 59 seconds'), + datetime.timedelta(days=1, hours=12, minutes=59, seconds=59) + ) + + self.assertEqual( + TimedeltaTest.delta.from_json('1 day 46799 seconds'), + datetime.timedelta(days=1, seconds=46799) + ) + + def test_to_json(self): + self.assertEqual( + '1 days 46799 seconds', + TimedeltaTest.delta.to_json(datetime.timedelta(days=1, hours=12, minutes=59, seconds=59)) + ) + + def test_serialize(self): + self.assertEqual( + TimedeltaTest.delta.serialize('1 day 12 hours 59 minutes 59 seconds'), + '"1 day 12 hours 59 minutes 59 seconds"' + ) + + def test_deserialize(self): + self.assertEqual( + '1 day 12 hours 59 minutes 59 seconds', + TimedeltaTest.delta.deserialize('1 day 12 hours 59 minutes 59 seconds') + ) + self.assertEqual( + '1 day 12 hours 59 minutes 59 seconds', + TimedeltaTest.delta.deserialize('"1 day 12 hours 59 minutes 59 seconds"') + ) diff --git a/common/test/data/full/course.xml b/common/test/data/full/course.xml index b2f9097020..9ee128da1a 100644 --- a/common/test/data/full/course.xml +++ b/common/test/data/full/course.xml @@ -1 +1 @@ - + diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index fb20fd2b22..668ac4804c 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -8,6 +8,6 @@ -e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk # Our libraries: --e git+https://github.com/edx/XBlock.git@a56a79d8#egg=XBlock +-e git+https://github.com/edx/XBlock.git@eaaf4831#egg=XBlock -e git+https://github.com/edx/codejail.git@5fb5fa0#egg=codejail -e git+https://github.com/edx/diff-cover.git@v0.1.0#egg=diff_cover From 6c24694a7ce16eea38d6af31a414aef70fad928a Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 6 Jun 2013 12:48:59 -0400 Subject: [PATCH 016/173] Fix tests that vary urls.py Create a mixin class that can be used for tests that customize urls.py to force django to reload it, so that they don't break other tests. --- common/djangoapps/mitxmako/tests.py | 9 ++--- common/djangoapps/util/testing.py | 34 +++++++++++++++++++ .../django_comment_client/base/tests.py | 15 +++++++- lms/envs/test.py | 6 ++-- 4 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 common/djangoapps/util/testing.py diff --git a/common/djangoapps/mitxmako/tests.py b/common/djangoapps/mitxmako/tests.py index f419daa681..e7e56a9472 100644 --- a/common/djangoapps/mitxmako/tests.py +++ b/common/djangoapps/mitxmako/tests.py @@ -1,18 +1,15 @@ from django.test import TestCase from django.test.utils import override_settings from django.core.urlresolvers import reverse -from django.conf import settings from mitxmako.shortcuts import marketing_link from mock import patch -from nose.plugins.skip import SkipTest +from util.testing import UrlResetMixin -class ShortcutsTests(TestCase): + +class ShortcutsTests(UrlResetMixin, TestCase): """ Test the mitxmako shortcuts file """ - # TODO: fix this test. It is causing intermittent test failures on - # subsequent tests due to the way urls are loaded - raise SkipTest() @override_settings(MKTG_URLS={'ROOT': 'dummy-root', 'ABOUT': '/about-us'}) @override_settings(MKTG_URL_LINK_MAP={'ABOUT': 'login'}) def test_marketing_link(self): diff --git a/common/djangoapps/util/testing.py b/common/djangoapps/util/testing.py new file mode 100644 index 0000000000..d33f1c8f8b --- /dev/null +++ b/common/djangoapps/util/testing.py @@ -0,0 +1,34 @@ +import sys + +from django.conf import settings +from django.core.urlresolvers import clear_url_caches + + +class UrlResetMixin(object): + """Mixin to reset urls.py before and after a test + + Django memoizes the function that reads the urls module (whatever module + urlconf names). The module itself is also stored by python in sys.modules. + To fully reload it, we need to reload the python module, and also clear django's + cache of the parsed urls. + + However, the order in which we do this doesn't matter, because neither one will + get reloaded until the next request + + Doing this is expensive, so it should only be added to tests that modify settings + that affect the contents of urls.py + """ + + def _reset_urls(self, urlconf=None): + if urlconf is None: + urlconf = settings.ROOT_URLCONF + + if urlconf in sys.modules: + reload(sys.modules[urlconf]) + clear_url_caches() + + def setUp(self): + """Reset django default urlconf before tests and after tests""" + super(UrlResetMixin, self).setUp() + self._reset_urls() + self.addCleanup(self._reset_urls) diff --git a/lms/djangoapps/django_comment_client/base/tests.py b/lms/djangoapps/django_comment_client/base/tests.py index 3e06402ddd..aa5b657bd6 100644 --- a/lms/djangoapps/django_comment_client/base/tests.py +++ b/lms/djangoapps/django_comment_client/base/tests.py @@ -1,5 +1,6 @@ import logging +from django.conf import settings from django.test.utils import override_settings from django.test.client import Client from django.contrib.auth.models import User @@ -8,6 +9,7 @@ from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from django.core.urlresolvers import reverse from django.core.management import call_command +from util.testing import UrlResetMixin from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from nose.tools import assert_true, assert_equal @@ -18,8 +20,19 @@ log = logging.getLogger(__name__) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @patch('comment_client.utils.requests.request') -class ViewsTestCase(ModuleStoreTestCase): +class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase): def setUp(self): + + # This feature affects the contents of urls.py, so we change + # it before the call to super.setUp() which reloads urls.py (because + # of the UrlResetMixin) + + # This setting is cleaned up at the end of the test by @override_settings, which + # restores all of the old settings + settings.MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True + + super(ViewsTestCase, self).setUp() + # create a course self.course = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') diff --git a/lms/envs/test.py b/lms/envs/test.py index 6691d50106..3ccfa24014 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -20,8 +20,10 @@ from path import path # can test everything else :) MITX_FEATURES['DISABLE_START_DATES'] = True -# Until we have discussion actually working in test mode, just turn it off -MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True +# Most tests don't use the discussion service, so we turn it off to speed them up. +# Tests that do can enable this flag, but must use the UrlResetMixin class to force urls.py +# to reload +MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = False MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True From a746a9ad1511e5d02cea41a63250d3409d7868a9 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Mon, 10 Jun 2013 11:02:19 -0400 Subject: [PATCH 017/173] Get rid of unused code --- common/lib/calc/calc.py | 44 +++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index cc3a883221..349810d4c9 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -21,7 +21,7 @@ from pyparsing import (Word, nums, Literal, ZeroOrMore, MatchFirst, Optional, Forward, CaselessLiteral, - NoMatch, stringEnd, Suppress, Combine) + stringEnd, Suppress, Combine) DEFAULT_FUNCTIONS = {'sin': numpy.sin, 'cos': numpy.cos, @@ -258,31 +258,27 @@ def evaluator(variables, functions, string, cs=False): # Predefine recursive variables expr = Forward() - # Handle variables passed in. E.g. if we have {'R':0.5}, we make the substitution. - # Special case for no variables because of how we understand PyParsing is put together - if len(all_variables) > 0: - # We sort the list so that var names (like "e2") match before - # mathematical constants (like "e"). This is kind of a hack. - all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True) - varnames = MatchFirst([CasedLiteral(k) for k in all_variables_keys]) - varnames.setParseAction( - lambda x: [all_variables[k] for k in x] - ) - else: - # all_variables includes DEFAULT_VARIABLES, which isn't empty - # this is unreachable. Get rid of it? - varnames = NoMatch() + # Handle variables passed in. + # E.g. if we have {'R':0.5}, we make the substitution. + # We sort the list so that var names (like "e2") match before + # mathematical constants (like "e"). This is kind of a hack. + all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True) + varnames = MatchFirst([CasedLiteral(k) for k in all_variables_keys]) + varnames.setParseAction( + lambda x: [all_variables[k] for k in x] + ) + + # if all_variables were empty, then pyparsing wants + # varnames = NoMatch() + # this is not the case, as all_variables contains the defaults # Same thing for functions. - if len(all_functions) > 0: - funcnames = MatchFirst([CasedLiteral(k) for k in all_functions.keys()]) - function = funcnames + Suppress("(") + expr + Suppress(")") - function.setParseAction( - lambda x: [all_functions[x[0]](x[1])] - ) - else: - # see note above (this is unreachable) - function = NoMatch() + all_functions_keys = sorted(all_functions.keys(), key=len, reverse=True) + funcnames = MatchFirst([CasedLiteral(k) for k in all_functions_keys]) + function = funcnames + Suppress("(") + expr + Suppress(")") + function.setParseAction( + lambda x: [all_functions[x[0]](x[1])] + ) atom = number | function | varnames | Suppress("(") + expr + Suppress(")") From 58e98d13cc5df9f0ff2994719fcf152219ada507 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Mon, 10 Jun 2013 11:34:59 -0400 Subject: [PATCH 018/173] Make Jenkins test the calc module --- jenkins/test.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/jenkins/test.sh b/jenkins/test.sh index 35be3a0121..127bf4fa1d 100755 --- a/jenkins/test.sh +++ b/jenkins/test.sh @@ -77,6 +77,7 @@ rake test_cms || TESTS_FAILED=1 rake test_lms || TESTS_FAILED=1 rake test_common/lib/capa || TESTS_FAILED=1 rake test_common/lib/xmodule || TESTS_FAILED=1 +rake test_common/lib/calc || TESTS_FAILED=1 # Run the javascript unit tests rake phantomjs_jasmine_lms || TESTS_FAILED=1 From ed90ed9a345aac35cbdc878ddc484475923c08fd Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 5 Jun 2013 15:36:49 -0400 Subject: [PATCH 019/173] Added tests for new math functions --- common/lib/calc/tests/test_calc.py | 99 ++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/common/lib/calc/tests/test_calc.py b/common/lib/calc/tests/test_calc.py index cfa1b7525d..e29c6776a9 100644 --- a/common/lib/calc/tests/test_calc.py +++ b/common/lib/calc/tests/test_calc.py @@ -194,6 +194,105 @@ class EvaluatorTest(unittest.TestCase): arctan_angles = arcsin_angles self.assert_function_values('arctan', arctan_inputs, arctan_angles) + def test_reciprocal_trig_functions(self): + """ + Test the reciprocal trig functions provided in calc.py + + which are: sec, csc, cot, arcsec, arccsc, arccot + """ + angles = ['-pi/4', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', '1 + j'] + sec_values = [1.414, 1.155, 1.236, -1.414, 1.414, 0.498+0.591j] + csc_values = [-1.414, 2, 1.701, -1.414, 1.414, 0.622-0.304j] + cot_values = [-1, 1.732, 1.376, 1, 1, 0.218-0.868j] + + self.assert_function_values('sec', angles, sec_values) + self.assert_function_values('csc', angles, csc_values) + self.assert_function_values('cot', angles, cot_values) + + arcsec_inputs = ['1.1547', '1.2361', '2', '-2', '-1.4142', '0.4983+0.5911*j'] + arcsec_angles = [0.524, 0.628, 1.047, 2.094, 2.356, 1 + 1j] + self.assert_function_values('arcsec', arcsec_inputs, arcsec_angles) + + arccsc_inputs = ['-1.1547', '-1.4142', '2', '1.7013', '1.1547', '0.6215-0.3039*j'] + arccsc_angles = [-1.047, -0.785, 0.524, 0.628, 1.047, 1 + 1j] + self.assert_function_values('arccsc', arccsc_inputs, arccsc_angles) + + # Has the same range as arccsc + arccot_inputs = ['-0.5774', '-1', '1.7321', '1.3764', '0.5774', '(0.2176-0.868*j)'] + arccot_angles = arccsc_angles + self.assert_function_values('arccot', arccot_inputs, arccot_angles) + + def test_hyperbolic_functions(self): + """ + Test the hyperbolic functions + + which are: sinh, cosh, tanh, sech, csch, coth + """ + inputs = ['0', '0.5', '1', '2', '1+j'] + neg_inputs = ['0', '-0.5', '-1', '-2', '-1-j'] + negate = lambda x: [-k for k in x] + + # sinh is odd + sinh_vals = [0, 0.521, 1.175, 3.627, 0.635 + 1.298j] + self.assert_function_values('sinh', inputs, sinh_vals) + self.assert_function_values('sinh', neg_inputs, negate(sinh_vals)) + + # cosh is even - do not negate + cosh_vals = [1, 1.128, 1.543, 3.762, 0.834 + 0.989j] + self.assert_function_values('cosh', inputs, cosh_vals) + self.assert_function_values('cosh', neg_inputs, cosh_vals) + + # tanh is odd + tanh_vals = [0, 0.462, 0.762, 0.964, 1.084 + 0.272j] + self.assert_function_values('tanh', inputs, tanh_vals) + self.assert_function_values('tanh', neg_inputs, negate(tanh_vals)) + + # sech is even - do not negate + sech_vals = [1, 0.887, 0.648, 0.266, 0.498 - 0.591j] + self.assert_function_values('sech', inputs, sech_vals) + self.assert_function_values('sech', neg_inputs, sech_vals) + + # the following functions do not have 0 in their domain + inputs = inputs[1:] + neg_inputs = neg_inputs[1:] + + # csch is odd + csch_vals = [1.919, 0.851, 0.276, 0.304 - 0.622j] + self.assert_function_values('csch', inputs, csch_vals) + self.assert_function_values('csch', neg_inputs, negate(csch_vals)) + + # coth is odd + coth_vals = [2.164, 1.313, 1.037, 0.868 - 0.218j] + self.assert_function_values('coth', inputs, coth_vals) + self.assert_function_values('coth', neg_inputs, negate(coth_vals)) + + def test_hyperbolic_inverses(self): + """ + Test the inverse hyperbolic functions + + which are of the form arc[X]h + """ + results = [0, 0.5, 1, 2, 1+1j] + + sinh_vals = ['0', '0.5211', '1.1752', '3.6269', '0.635+1.2985*j'] + self.assert_function_values('arcsinh', sinh_vals, results) + + cosh_vals = ['1', '1.1276', '1.5431', '3.7622', '0.8337+0.9889*j'] + self.assert_function_values('arccosh', cosh_vals, results) + + tanh_vals = ['0', '0.4621', '0.7616', '0.964', '1.0839+0.2718*j'] + self.assert_function_values('arctanh', tanh_vals, results) + + sech_vals = ['1.0', '0.8868', '0.6481', '0.2658', '0.4983-0.5911*j'] + self.assert_function_values('arcsech', sech_vals, results) + + results = results[1:] + csch_vals = ['1.919', '0.8509', '0.2757', '0.3039-0.6215*j'] + self.assert_function_values('arccsch', csch_vals, results) + + coth_vals = ['2.164', '1.313', '1.0373', '0.868-0.2176*j'] + self.assert_function_values('arccoth', coth_vals, results) + def test_other_functions(self): """ Test the non-trig functions provided in calc.py From 944e3390e0f4f63b90e87b65e529115c8d8b26e0 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Mon, 3 Jun 2013 17:20:52 -0400 Subject: [PATCH 020/173] Add support for various math functions in calc.py. --- common/lib/calc/calc.py | 22 ++++++- common/lib/calc/calcfunctions.py | 99 ++++++++++++++++++++++++++++++ common/lib/calc/tests/test_calc.py | 8 +-- 3 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 common/lib/calc/calcfunctions.py diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 349810d4c9..d3874639bc 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -12,6 +12,7 @@ import re import numpy import scipy.constants +import calcfunctions # have numpy raise errors on functions outside its domain # See http://docs.scipy.org/doc/numpy/reference/generated/numpy.seterr.html @@ -26,16 +27,35 @@ from pyparsing import (Word, nums, Literal, DEFAULT_FUNCTIONS = {'sin': numpy.sin, 'cos': numpy.cos, 'tan': numpy.tan, + 'sec': calcfunctions.sec, + 'csc': calcfunctions.csc, + 'cot': calcfunctions.cot, 'sqrt': numpy.sqrt, 'log10': numpy.log10, 'log2': numpy.log2, 'ln': numpy.log, + 'exp': numpy.exp, 'arccos': numpy.arccos, 'arcsin': numpy.arcsin, 'arctan': numpy.arctan, + 'arcsec': calcfunctions.arcsec, + 'arccsc': calcfunctions.arccsc, + 'arccot': calcfunctions.arccot, 'abs': numpy.abs, 'fact': math.factorial, - 'factorial': math.factorial + 'factorial': math.factorial, + 'sinh': numpy.sinh, + 'cosh': numpy.cosh, + 'tanh': numpy.tanh, + 'sech': calcfunctions.sech, + 'csch': calcfunctions.csch, + 'coth': calcfunctions.coth, + 'arcsinh': numpy.arcsinh, + 'arccosh': numpy.arccosh, + 'arctanh': numpy.arctanh, + 'arcsech': calcfunctions.arcsech, + 'arccsch': calcfunctions.arccsch, + 'arccoth': calcfunctions.arccoth } DEFAULT_VARIABLES = {'j': numpy.complex(0, 1), 'e': numpy.e, diff --git a/common/lib/calc/calcfunctions.py b/common/lib/calc/calcfunctions.py new file mode 100644 index 0000000000..d0ac4f7a3d --- /dev/null +++ b/common/lib/calc/calcfunctions.py @@ -0,0 +1,99 @@ +""" +Provide the mathematical functions that numpy doesn't. + +Specifically, the secant/cosecant/cotangents and their inverses and +hyperbolic counterparts +""" +import numpy + + +# Normal Trig +def sec(arg): + """ + Secant + """ + return 1 / numpy.cos(arg) + + +def csc(arg): + """ + Cosecant + """ + return 1 / numpy.sin(arg) + + +def cot(arg): + """ + Cotangent + """ + return 1 / numpy.tan(arg) + + +# Inverse Trig +# http://en.wikipedia.org/wiki/Inverse_trigonometric_functions#Relationships_among_the_inverse_trigonometric_functions +def arcsec(val): + """ + Inverse secant + """ + return numpy.arccos(1. / val) + + +def arccsc(val): + """ + Inverse cosecant + """ + return numpy.arcsin(1. / val) + + +def arccot(val): + """ + Inverse cotangent + """ + if numpy.real(val) < 0: + return -numpy.pi / 2 - numpy.arctan(val) + else: + return numpy.pi / 2 - numpy.arctan(val) + + +# Hyperbolic Trig +def sech(arg): + """ + Hyperbolic secant + """ + return 1 / numpy.cosh(arg) + + +def csch(arg): + """ + Hyperbolic cosecant + """ + return 1 / numpy.sinh(arg) + + +def coth(arg): + """ + Hyperbolic cotangent + """ + return 1 / numpy.tanh(arg) + + +# And their inverses +def arcsech(val): + """ + Inverse hyperbolic secant + """ + return numpy.arccosh(1. / val) + + +def arccsch(val): + """ + Inverse hyperbolic cosecant + """ + return numpy.arcsinh(1. / val) + + +def arccoth(val): + """ + Inverse hyperbolic cotangent + """ + return numpy.arctanh(1. / val) diff --git a/common/lib/calc/tests/test_calc.py b/common/lib/calc/tests/test_calc.py index e29c6776a9..13cd9e9471 100644 --- a/common/lib/calc/tests/test_calc.py +++ b/common/lib/calc/tests/test_calc.py @@ -201,9 +201,9 @@ class EvaluatorTest(unittest.TestCase): which are: sec, csc, cot, arcsec, arccsc, arccot """ angles = ['-pi/4', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', '1 + j'] - sec_values = [1.414, 1.155, 1.236, -1.414, 1.414, 0.498+0.591j] - csc_values = [-1.414, 2, 1.701, -1.414, 1.414, 0.622-0.304j] - cot_values = [-1, 1.732, 1.376, 1, 1, 0.218-0.868j] + sec_values = [1.414, 1.155, 1.236, -1.414, 1.414, 0.498 + 0.591j] + csc_values = [-1.414, 2, 1.701, -1.414, 1.414, 0.622 - 0.304j] + cot_values = [-1, 1.732, 1.376, 1, 1, 0.218 - 0.868j] self.assert_function_values('sec', angles, sec_values) self.assert_function_values('csc', angles, csc_values) @@ -272,7 +272,7 @@ class EvaluatorTest(unittest.TestCase): which are of the form arc[X]h """ - results = [0, 0.5, 1, 2, 1+1j] + results = [0, 0.5, 1, 2, 1 + 1j] sinh_vals = ['0', '0.5211', '1.1752', '3.6269', '0.635+1.2985*j'] self.assert_function_values('arcsinh', sinh_vals, results) From 0f72eedd37285e59a2e93932e4a2c1c967053376 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Mon, 10 Jun 2013 10:51:17 -0400 Subject: [PATCH 021/173] Add variable i as an imaginary unit --- common/lib/calc/calc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index d3874639bc..3afc0f91bc 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -57,7 +57,8 @@ DEFAULT_FUNCTIONS = {'sin': numpy.sin, 'arccsch': calcfunctions.arccsch, 'arccoth': calcfunctions.arccoth } -DEFAULT_VARIABLES = {'j': numpy.complex(0, 1), +DEFAULT_VARIABLES = {'i': numpy.complex(0, 1), + 'j': numpy.complex(0, 1), 'e': numpy.e, 'pi': numpy.pi, 'k': scipy.constants.k, From 17c9c104d90680f270529d725a27504651535e80 Mon Sep 17 00:00:00 2001 From: cahrens Date: Mon, 10 Jun 2013 12:29:49 -0400 Subject: [PATCH 022/173] Update version of xblock. --- requirements/edx/github.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 668ac4804c..36fd9dca06 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -8,6 +8,6 @@ -e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk # Our libraries: --e git+https://github.com/edx/XBlock.git@eaaf4831#egg=XBlock +-e git+https://github.com/edx/XBlock.git@4c5d2397#egg=XBlock -e git+https://github.com/edx/codejail.git@5fb5fa0#egg=codejail -e git+https://github.com/edx/diff-cover.git@v0.1.0#egg=diff_cover From 8f49783da0e8eeb5d6e55948c5804cdafff26e01 Mon Sep 17 00:00:00 2001 From: John Kern Date: Mon, 10 Jun 2013 12:58:57 -0700 Subject: [PATCH 023/173] encoded URL to fix formating --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a6236ea70..92a4116354 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ CMS templates. Fortunately, `rake` will do all of this for you! Just run: If you are running these commands using the [`zsh`](http://www.zsh.org/) shell, zsh will assume that you are doing -[shell globbing](https://en.wikipedia.org/wiki/Glob_(programming)), search for +[shell globbing](https://en.wikipedia.org/wiki/Glob_%28programming%29), search for a file in your directory named `django-adminsyncdb` or `django-adminmigrate`, and fail. To fix this, just surround the argument with quotation marks, so that you're running `rake "django-admin[syncdb]"`. From d7194e6bec3de27c35412e15da7e6c1cc3edc666 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Wed, 29 May 2013 10:19:16 -0400 Subject: [PATCH 024/173] struct_time to datetime conversion. --- .../contentstore/tests/test_contentstore.py | 6 +-- .../tests/test_course_settings.py | 10 +--- cms/djangoapps/contentstore/views/assets.py | 6 +-- cms/djangoapps/contentstore/views/course.py | 33 ++++++------ .../models/settings/course_details.py | 20 +++---- cms/templates/edit_subsection.html | 20 ++++--- cms/templates/overview.html | 9 ++-- common/djangoapps/contentserver/middleware.py | 7 +-- .../commands/pearson_make_tc_registration.py | 5 +- common/djangoapps/student/models.py | 17 +++--- common/djangoapps/xmodule_modifiers.py | 9 ++-- common/lib/capa/capa/inputtypes.py | 12 ++--- common/lib/xmodule/xmodule/capa_module.py | 8 +-- common/lib/xmodule/xmodule/course_module.py | 45 ++++++++-------- common/lib/xmodule/xmodule/fields.py | 24 ++++++--- common/lib/xmodule/xmodule/foldit_module.py | 9 ++-- .../lib/xmodule/xmodule/modulestore/draft.py | 3 +- common/lib/xmodule/xmodule/modulestore/xml.py | 6 +-- .../openendedchild.py | 35 +++++++------ .../xmodule/xmodule/peer_grading_module.py | 47 +++++++++-------- .../xmodule/tests/test_course_module.py | 11 ++-- .../xmodule/xmodule/tests/test_date_utils.py | 35 ------------- .../lib/xmodule/xmodule/tests/test_fields.py | 25 +++------ .../lib/xmodule/xmodule/tests/test_import.py | 29 ++++++++--- common/lib/xmodule/xmodule/timeinfo.py | 3 +- common/lib/xmodule/xmodule/timeparse.py | 11 ++-- common/lib/xmodule/xmodule/util/date_utils.py | 40 +++++--------- .../Administrivia_and_Circuit_Elements.xml | 52 +++++++++++-------- lms/djangoapps/courseware/access.py | 20 +++---- .../courseware/tests/test_access.py | 18 +++---- lms/djangoapps/courseware/tests/tests.py | 41 +++++++-------- lms/djangoapps/django_comment_client/utils.py | 16 ++---- 32 files changed, 293 insertions(+), 339 deletions(-) delete mode 100644 common/lib/xmodule/xmodule/tests/test_date_utils.py diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 232b68ecc8..1e6ab8cd86 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -271,7 +271,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ) self.assertTrue(getattr(draft_problem, 'is_draft', False)) - #now requery with depth + # now requery with depth course = modulestore('draft').get_item( Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]), depth=None @@ -539,7 +539,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): on_disk = loads(grading_policy.read()) self.assertEqual(on_disk, course.grading_policy) - #check for policy.json + # check for policy.json self.assertTrue(filesystem.exists('policy.json')) # compare what's on disk to what we have in the course module @@ -990,7 +990,7 @@ class ContentStoreTest(ModuleStoreTestCase): def test_metadata_inheritance(self): module_store = modulestore('direct') - import_from_xml(module_store, 'common/test/data/', ['full']) + import_from_xml(module_store, 'common/test/data/', ['full'], verbose=True) course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 2a4ff46038..c1b7a9fa0e 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -151,22 +151,16 @@ class CourseDetailsViewTest(CourseTestCase): self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==") self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==") - @staticmethod - def struct_to_datetime(struct_time): - return datetime.datetime(*struct_time[:6], tzinfo=UTC()) - def compare_date_fields(self, details, encoded, context, field): if details[field] is not None: date = Date() if field in encoded and encoded[field] is not None: - encoded_encoded = date.from_json(encoded[field]) - dt1 = CourseDetailsViewTest.struct_to_datetime(encoded_encoded) + dt1 = date.from_json(encoded[field]) if isinstance(details[field], datetime.datetime): dt2 = details[field] else: - details_encoded = date.from_json(details[field]) - dt2 = CourseDetailsViewTest.struct_to_datetime(details_encoded) + dt2 = date.from_json(details[field]) expected_delta = datetime.timedelta(0) self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context) diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py index b5041d3e9f..2be2cfafb8 100644 --- a/cms/djangoapps/contentstore/views/assets.py +++ b/cms/djangoapps/contentstore/views/assets.py @@ -62,7 +62,7 @@ def asset_index(request, org, course, name): asset_id = asset['_id'] display_info = {} display_info['displayname'] = asset['displayname'] - display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple()) + display_info['uploadDate'] = get_default_time_display(asset['uploadDate']) asset_location = StaticContent.compute_location(asset_id['org'], asset_id['course'], asset_id['name']) display_info['url'] = StaticContent.get_url_path_from_location(asset_location) @@ -131,7 +131,7 @@ def upload_asset(request, org, course, coursename): readback = contentstore().find(content.location) response_payload = {'displayname': content.name, - 'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()), + 'uploadDate': get_default_time_display(readback.last_modified_at), 'url': StaticContent.get_url_path_from_location(content.location), 'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None, 'msg': 'Upload completed' @@ -231,7 +231,7 @@ def generate_export_course(request, org, course, name): logging.debug('root = {0}'.format(root_dir)) export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore()) - #filename = root_dir / name + '.tar.gz' + # filename = root_dir / name + '.tar.gz' logging.debug('tar file being generated at {0}'.format(export_file.name)) tar_file = tarfile.open(name=export_file.name, mode='w:gz') diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 07f6b9669c..e1c176eebe 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -2,7 +2,6 @@ Views related to operations on course objects """ import json -import time from django.contrib.auth.decorators import login_required from django_future.csrf import ensure_csrf_cookie @@ -32,6 +31,8 @@ from .component import OPEN_ENDED_COMPONENT_TYPES, \ NOTE_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY from django_comment_common.utils import seed_permissions_roles +import datetime +from django.utils.timezone import UTC # TODO: should explicitly enumerate exports with __all__ @@ -130,7 +131,7 @@ def create_new_course(request): new_course.display_name = display_name # set a default start date to now - new_course.start = time.gmtime() + new_course.start = datetime.datetime.now(UTC()) initialize_course_tabs(new_course) @@ -357,49 +358,49 @@ def course_advanced_updates(request, org, course, name): # Whether or not to filter the tabs key out of the settings metadata filter_tabs = True - #Check to see if the user instantiated any advanced components. This is a hack - #that does the following : - # 1) adds/removes the open ended panel tab to a course automatically if the user + # Check to see if the user instantiated any advanced components. This is a hack + # that does the following : + # 1) adds/removes the open ended panel tab to a course automatically if the user # has indicated that they want to edit the combinedopendended or peergrading module # 2) adds/removes the notes panel tab to a course automatically if the user has # indicated that they want the notes module enabled in their course # TODO refactor the above into distinct advanced policy settings if ADVANCED_COMPONENT_POLICY_KEY in request_body: - #Get the course so that we can scrape current tabs + # Get the course so that we can scrape current tabs course_module = modulestore().get_item(location) - #Maps tab types to components + # Maps tab types to components tab_component_map = { - 'open_ended': OPEN_ENDED_COMPONENT_TYPES, + 'open_ended': OPEN_ENDED_COMPONENT_TYPES, 'notes': NOTE_COMPONENT_TYPES, } - #Check to see if the user instantiated any notes or open ended components + # Check to see if the user instantiated any notes or open ended components for tab_type in tab_component_map.keys(): component_types = tab_component_map.get(tab_type) found_ac_type = False for ac_type in component_types: if ac_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: - #Add tab to the course if needed + # Add tab to the course if needed changed, new_tabs = add_extra_panel_tab(tab_type, course_module) - #If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json + # If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json if changed: course_module.tabs = new_tabs request_body.update({'tabs': new_tabs}) - #Indicate that tabs should not be filtered out of the metadata + # Indicate that tabs should not be filtered out of the metadata filter_tabs = False - #Set this flag to avoid the tab removal code below. + # Set this flag to avoid the tab removal code below. found_ac_type = True break - #If we did not find a module type in the advanced settings, + # If we did not find a module type in the advanced settings, # we may need to remove the tab from the course. if not found_ac_type: - #Remove tab from the course if needed + # Remove tab from the course if needed changed, new_tabs = remove_extra_panel_tab(tab_type, course_module) if changed: course_module.tabs = new_tabs request_body.update({'tabs': new_tabs}) - #Indicate that tabs should *not* be filtered out of the metadata + # Indicate that tabs should *not* be filtered out of the metadata filter_tabs = False response_json = json.dumps(CourseMetadata.update_from_json(location, diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index 0dbb47b31b..28dba473f2 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -3,26 +3,26 @@ from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.inheritance import own_metadata import json from json.encoder import JSONEncoder -import time from contentstore.utils import get_modulestore from models.settings import course_grading from contentstore.utils import update_item from xmodule.fields import Date import re import logging +import datetime class CourseDetails(object): def __init__(self, location): - self.course_location = location # a Location obj + self.course_location = location # a Location obj self.start_date = None # 'start' - self.end_date = None # 'end' + self.end_date = None # 'end' self.enrollment_start = None self.enrollment_end = None - self.syllabus = None # a pdf file asset - self.overview = "" # html to render as the overview - self.intro_video = None # a video pointer - self.effort = None # int hours/week + self.syllabus = None # a pdf file asset + self.overview = "" # html to render as the overview + self.intro_video = None # a video pointer + self.effort = None # int hours/week @classmethod def fetch(cls, course_location): @@ -73,9 +73,9 @@ class CourseDetails(object): """ Decode the json into CourseDetails and save any changed attrs to the db """ - ## TODO make it an error for this to be undefined & for it to not be retrievable from modulestore + # # TODO make it an error for this to be undefined & for it to not be retrievable from modulestore course_location = jsondict['course_location'] - ## Will probably want to cache the inflight courses because every blur generates an update + # # Will probably want to cache the inflight courses because every blur generates an update descriptor = get_modulestore(course_location).get_item(course_location) dirty = False @@ -181,7 +181,7 @@ class CourseSettingsEncoder(json.JSONEncoder): return obj.__dict__ elif isinstance(obj, Location): return obj.dict() - elif isinstance(obj, time.struct_time): + elif isinstance(obj, datetime.datetime): return Date().to_json(obj) else: return JSONEncoder.default(self, obj) diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index 9bb9b3a506..4aae070ca1 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -1,7 +1,7 @@ <%inherit file="base.html" /> <%! import logging - from xmodule.util.date_utils import get_time_struct_display + from xmodule.util.date_utils import get_default_time_display %> <%! from django.core.urlresolvers import reverse %> @@ -36,11 +36,15 @@
- +
- +
% if subsection.lms.start != parent_item.lms.start and subsection.lms.start: @@ -48,7 +52,7 @@

The date above differs from the release date of ${parent_item.display_name_with_default}, which is unset. % else:

The date above differs from the release date of ${parent_item.display_name_with_default} – - ${get_time_struct_display(parent_item.lms.start, '%m/%d/%Y at %H:%M UTC')}. + ${get_default_time_display(parent_item.lms.start)}. % endif Sync to ${parent_item.display_name_with_default}.

% endif @@ -65,11 +69,15 @@
- +
- +
Remove due date
diff --git a/cms/templates/overview.html b/cms/templates/overview.html index d327c8b324..0b82d76943 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -1,7 +1,7 @@ <%inherit file="base.html" /> <%! import logging - from xmodule.util.date_utils import get_time_struct_display + from xmodule.util import date_utils %> <%! from django.core.urlresolvers import reverse %> <%block name="title">Course Outline @@ -154,14 +154,15 @@

diff --git a/common/djangoapps/contentserver/middleware.py b/common/djangoapps/contentserver/middleware.py index 8e9e70046d..7deb0901aa 100644 --- a/common/djangoapps/contentserver/middleware.py +++ b/common/djangoapps/contentserver/middleware.py @@ -1,7 +1,4 @@ -import logging -import time - -from django.http import HttpResponse, Http404, HttpResponseNotModified +from django.http import HttpResponse, HttpResponseNotModified from xmodule.contentstore.django import contentstore from xmodule.contentstore.content import StaticContent, XASSET_LOCATION_TAG @@ -20,7 +17,7 @@ class StaticContentServer(object): # return a 'Bad Request' to browser as we have a malformed Location response = HttpResponse() response.status_code = 400 - return response + return response # first look in our cache so we don't have to round-trip to the DB content = get_cached_content(loc) diff --git a/common/djangoapps/student/management/commands/pearson_make_tc_registration.py b/common/djangoapps/student/management/commands/pearson_make_tc_registration.py index b10cf143a0..50e56bb4be 100644 --- a/common/djangoapps/student/management/commands/pearson_make_tc_registration.py +++ b/common/djangoapps/student/management/commands/pearson_make_tc_registration.py @@ -1,5 +1,4 @@ from optparse import make_option -from time import strftime from django.contrib.auth.models import User from django.core.management.base import BaseCommand, CommandError @@ -128,8 +127,8 @@ class Command(BaseCommand): exam = CourseDescriptor.TestCenterExam(course_id, exam_name, exam_info) # update option values for date_first and date_last to use YYYY-MM-DD format # instead of YYYY-MM-DDTHH:MM - our_options['eligibility_appointment_date_first'] = strftime("%Y-%m-%d", exam.first_eligible_appointment_date) - our_options['eligibility_appointment_date_last'] = strftime("%Y-%m-%d", exam.last_eligible_appointment_date) + our_options['eligibility_appointment_date_first'] = exam.first_eligible_appointment_date.strftime("%Y-%m-%d") + our_options['eligibility_appointment_date_last'] = exam.last_eligible_appointment_date.strftime("%Y-%m-%d") if exam is None: raise CommandError("Exam for course_id {} does not exist".format(course_id)) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index ab68b05f4b..57f3d756b9 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -16,7 +16,6 @@ import json import logging import uuid from random import randint -from time import strftime from django.conf import settings @@ -54,7 +53,7 @@ class UserProfile(models.Model): class Meta: db_table = "auth_userprofile" - ## CRITICAL TODO/SECURITY + # # CRITICAL TODO/SECURITY # Sanitize all fields. # This is not visible to other users, but could introduce holes later user = models.OneToOneField(User, unique=True, db_index=True, related_name='profile') @@ -429,8 +428,8 @@ class TestCenterRegistration(models.Model): registration.course_id = exam.course_id registration.accommodation_request = accommodation_request.strip() registration.exam_series_code = exam.exam_series_code - registration.eligibility_appointment_date_first = strftime("%Y-%m-%d", exam.first_eligible_appointment_date) - registration.eligibility_appointment_date_last = strftime("%Y-%m-%d", exam.last_eligible_appointment_date) + registration.eligibility_appointment_date_first = exam.first_eligible_appointment_date.strftime("%Y-%m-%d") + registration.eligibility_appointment_date_last = exam.last_eligible_appointment_date.strftime("%Y-%m-%d") registration.client_authorization_id = cls._create_client_authorization_id() # accommodation_code remains blank for now, along with Pearson confirmation information return registration @@ -598,7 +597,7 @@ def unique_id_for_user(user): return h.hexdigest() -## TODO: Should be renamed to generic UserGroup, and possibly +# # TODO: Should be renamed to generic UserGroup, and possibly # Given an optional field for type of group class UserTestGroup(models.Model): users = models.ManyToManyField(User, db_index=True) @@ -626,7 +625,7 @@ class Registration(models.Model): def activate(self): self.user.is_active = True self.user.save() - #self.delete() + # self.delete() class PendingNameChange(models.Model): @@ -648,7 +647,7 @@ class CourseEnrollment(models.Model): created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) class Meta: - unique_together = (('user', 'course_id'), ) + unique_together = (('user', 'course_id'),) def __unicode__(self): return "[CourseEnrollment] %s: %s (%s)" % (self.user, self.course_id, self.created) @@ -667,12 +666,12 @@ class CourseEnrollmentAllowed(models.Model): created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) class Meta: - unique_together = (('email', 'course_id'), ) + unique_together = (('email', 'course_id'),) def __unicode__(self): return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created) -#cache_relation(User.profile) +# cache_relation(User.profile) #### Helper methods for use from python manage.py shell and other classes. diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py index 45691cd854..570b38c942 100644 --- a/common/djangoapps/xmodule_modifiers.py +++ b/common/djangoapps/xmodule_modifiers.py @@ -1,7 +1,6 @@ import re import json import logging -import time import static_replace from django.conf import settings @@ -9,6 +8,8 @@ from functools import wraps from mitxmako.shortcuts import render_to_string from xmodule.seq_module import SequenceModule from xmodule.vertical_module import VerticalModule +import datetime +from django.utils.timezone import UTC log = logging.getLogger("mitx.xmodule_modifiers") @@ -83,7 +84,7 @@ def grade_histogram(module_id): cursor.execute(q, [module_id]) grades = list(cursor.fetchall()) - grades.sort(key=lambda x: x[0]) # Add ORDER BY to sql query? + grades.sort(key=lambda x: x[0]) # Add ORDER BY to sql query? if len(grades) >= 1 and grades[0][0] is None: return [] return grades @@ -101,7 +102,7 @@ def add_histogram(get_html, module, user): @wraps(get_html) def _get_html(): - if type(module) in [SequenceModule, VerticalModule]: # TODO: make this more general, eg use an XModule attribute instead + if type(module) in [SequenceModule, VerticalModule]: # TODO: make this more general, eg use an XModule attribute instead return get_html() module_id = module.id @@ -132,7 +133,7 @@ def add_histogram(get_html, module, user): # useful to indicate to staff if problem has been released or not # TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here - now = time.gmtime() + now = datetime.datetime.now(UTC()) is_released = "unknown" mstart = module.descriptor.lms.start diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 65280d6d29..3680379406 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -144,11 +144,11 @@ class InputTypeBase(object): self.tag = xml.tag self.system = system - ## NOTE: ID should only come from one place. If it comes from multiple, - ## we use state first, XML second (in case the xml changed, but we have - ## existing state with an old id). Since we don't make this guarantee, - ## we can swap this around in the future if there's a more logical - ## order. + # # NOTE: ID should only come from one place. If it comes from multiple, + # # we use state first, XML second (in case the xml changed, but we have + # # existing state with an old id). Since we don't make this guarantee, + # # we can swap this around in the future if there's a more logical + # # order. self.input_id = state.get('id', xml.get('id')) if self.input_id is None: @@ -769,7 +769,7 @@ class MatlabInput(CodeInput): # construct xqueue headers qinterface = self.system.xqueue['interface'] - qtime = datetime.strftime(datetime.utcnow(), xqueue_interface.dateformat) + qtime = datetime.utcnow().strftime(xqueue_interface.dateformat) callback_url = self.system.xqueue['construct_callback']('ungraded_response') anonymous_student_id = self.system.anonymous_student_id queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 9e0ab16203..51b20c12ea 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -11,7 +11,7 @@ import sys from pkg_resources import resource_string from capa.capa_problem import LoncapaProblem -from capa.responsetypes import StudentInputError,\ +from capa.responsetypes import StudentInputError, \ ResponseError, LoncapaProblemError from capa.util import convert_files_to_filenames from .progress import Progress @@ -20,7 +20,7 @@ from xmodule.raw_module import RawDescriptor from xmodule.exceptions import NotFoundError, ProcessingError from xblock.core import Scope, String, Boolean, Object from .fields import Timedelta, Date, StringyInteger, StringyFloat -from xmodule.util.date_utils import time_to_datetime +from django.utils.timezone import UTC log = logging.getLogger("mitx.courseware") @@ -134,7 +134,7 @@ class CapaModule(CapaFields, XModule): def __init__(self, system, location, descriptor, model_data): XModule.__init__(self, system, location, descriptor, model_data) - due_date = time_to_datetime(self.due) + due_date = self.due if self.graceperiod is not None and due_date: self.close_date = due_date + self.graceperiod @@ -502,7 +502,7 @@ class CapaModule(CapaFields, XModule): 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) + datetime.datetime.now(UTC()) > self.close_date) def closed(self): ''' Is the student still allowed to submit answers? ''' diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 063e53aef4..66d53b43ec 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -4,7 +4,6 @@ from math import exp from lxml import etree from path import path # NOTE (THK): Only used for detecting presence of syllabus import requests -import time from datetime import datetime import dateutil.parser @@ -14,11 +13,11 @@ from xmodule.seq_module import SequenceDescriptor, SequenceModule from xmodule.timeparse import parse_time from xmodule.util.decorators import lazyproperty from xmodule.graders import grader_from_conf -from xmodule.util.date_utils import time_to_datetime import json from xblock.core import Scope, List, String, Object, Boolean from .fields import Date +from django.utils.timezone import UTC log = logging.getLogger(__name__) @@ -219,8 +218,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): msg = None if self.start is None: msg = "Course loaded without a valid start date. id = %s" % self.id - # hack it -- start in 1970 - self.start = time.gmtime(0) + self.start = datetime.now(UTC()) log.critical(msg) self.system.error_tracker(msg) @@ -392,7 +390,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): textbook_xml_object.set('book_url', textbook.book_url) xml_object.append(textbook_xml_object) - + return xml_object def has_ended(self): @@ -403,10 +401,10 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): if self.end is None: return False - return time.gmtime() > self.end + return datetime.now(UTC()) > self.end def has_started(self): - return time.gmtime() > self.start + return datetime.now(UTC()) > self.start @property def grader(self): @@ -547,14 +545,16 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): announcement = self.announcement if announcement is not None: - announcement = time_to_datetime(announcement) + announcement = announcement try: start = dateutil.parser.parse(self.advertised_start) + if start.tzinfo is None: + start = start.replace(tzinfo=UTC()) except (ValueError, AttributeError): - start = time_to_datetime(self.start) + start = self.start - now = datetime.utcnow() + now = datetime.now(UTC()) return announcement, start, now @@ -656,7 +656,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): elif self.advertised_start is None and self.start is None: return 'TBD' else: - return time.strftime("%b %d, %Y", self.advertised_start or self.start) + return (self.advertised_start or self.start).strftime("%b %d, %Y") @property def end_date_text(self): @@ -665,7 +665,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): If the course does not have an end date set (course.end is None), an empty string will be returned. """ - return '' if self.end is None else time.strftime("%b %d, %Y", self.end) + return '' if self.end is None else self.end.strftime("%b %d, %Y") @property def forum_posts_allowed(self): @@ -673,7 +673,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): blackout_periods = [(parse_time(start), parse_time(end)) for start, end in self.discussion_blackouts] - now = time.gmtime() + now = datetime.now(UTC()) for start, end in blackout_periods: if start <= now <= end: return False @@ -699,7 +699,8 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date if self.last_eligible_appointment_date is None: raise ValueError("Last appointment date must be specified") - self.registration_start_date = self._try_parse_time('Registration_Start_Date') or time.gmtime(0) + self.registration_start_date = (self._try_parse_time('Registration_Start_Date') or + datetime.utcfromtimestamp(0)) self.registration_end_date = self._try_parse_time('Registration_End_Date') or self.last_eligible_appointment_date # do validation within the exam info: if self.registration_start_date > self.registration_end_date: @@ -725,32 +726,32 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): return None def has_started(self): - return time.gmtime() > self.first_eligible_appointment_date + return datetime.now(UTC()) > self.first_eligible_appointment_date def has_ended(self): - return time.gmtime() > self.last_eligible_appointment_date + return datetime.now(UTC()) > self.last_eligible_appointment_date def has_started_registration(self): - return time.gmtime() > self.registration_start_date + return datetime.now(UTC()) > self.registration_start_date def has_ended_registration(self): - return time.gmtime() > self.registration_end_date + return datetime.now(UTC()) > self.registration_end_date def is_registering(self): - now = time.gmtime() + now = datetime.now(UTC()) return now >= self.registration_start_date and now <= self.registration_end_date @property def first_eligible_appointment_date_text(self): - return time.strftime("%b %d, %Y", self.first_eligible_appointment_date) + return datetime.strftime("%b %d, %Y", self.first_eligible_appointment_date) @property def last_eligible_appointment_date_text(self): - return time.strftime("%b %d, %Y", self.last_eligible_appointment_date) + return datetime.strftime("%b %d, %Y", self.last_eligible_appointment_date) @property def registration_end_date_text(self): - return time.strftime("%b %d, %Y at %H:%M UTC", self.registration_end_date) + return datetime.strftime("%b %d, %Y at %H:%M UTC", self.registration_end_date) @property def current_test_center_exam(self): diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py index 3d56b7941e..3164c062bb 100644 --- a/common/lib/xmodule/xmodule/fields.py +++ b/common/lib/xmodule/xmodule/fields.py @@ -2,19 +2,19 @@ import time import logging import re -from datetime import timedelta from xblock.core import ModelType import datetime import dateutil.parser from xblock.core import Integer, Float, Boolean +from django.utils.timezone import UTC log = logging.getLogger(__name__) class Date(ModelType): ''' - Date fields know how to parse and produce json (iso) compatible formats. + Date fields know how to parse and produce json (iso) compatible formats. Converts to tz aware datetimes. ''' def from_json(self, field): """ @@ -27,11 +27,15 @@ class Date(ModelType): elif field is "": return None elif isinstance(field, basestring): - d = dateutil.parser.parse(field) - return d.utctimetuple() + result = dateutil.parser.parse(field) + if result.tzinfo is None: + result = result.replace(tzinfo=UTC()) + return result elif isinstance(field, (int, long, float)): - return time.gmtime(field / 1000) + return datetime.datetime.fromtimestamp(field / 1000, UTC()) elif isinstance(field, time.struct_time): + return datetime.datetime.fromtimestamp(time.mktime(field), UTC()) + elif isinstance(field, datetime.datetime): return field else: msg = "Field {0} has bad value '{1}'".format( @@ -49,7 +53,11 @@ class Date(ModelType): # struct_times are always utc return time.strftime('%Y-%m-%dT%H:%M:%SZ', value) elif isinstance(value, datetime.datetime): - return value.isoformat() + 'Z' + if value.tzinfo is None or value.utcoffset().total_seconds() == 0: + # isoformat adds +00:00 rather than Z + return value.strftime('%Y-%m-%dT%H:%M:%SZ') + else: + return value.isoformat() TIMEDELTA_REGEX = re.compile(r'^((?P\d+?) day(?:s?))?(\s)?((?P\d+?) hour(?:s?))?(\s)?((?P\d+?) minute(?:s)?)?(\s)?((?P\d+?) second(?:s)?)?$') @@ -74,7 +82,7 @@ class Timedelta(ModelType): for (name, param) in parts.iteritems(): if param: time_params[name] = int(param) - return timedelta(**time_params) + return datetime.timedelta(**time_params) def to_json(self, value): values = [] @@ -93,7 +101,7 @@ class StringyInteger(Integer): def from_json(self, value): try: return int(value) - except: + except Exception: return None diff --git a/common/lib/xmodule/xmodule/foldit_module.py b/common/lib/xmodule/xmodule/foldit_module.py index 62c5ea416e..5ab1b327c6 100644 --- a/common/lib/xmodule/xmodule/foldit_module.py +++ b/common/lib/xmodule/xmodule/foldit_module.py @@ -8,7 +8,6 @@ from xmodule.x_module import XModule from xmodule.xml_module import XmlDescriptor from xblock.core import Scope, Integer, String from .fields import Date -from xmodule.util.date_utils import time_to_datetime log = logging.getLogger(__name__) @@ -31,9 +30,7 @@ class FolditModule(FolditFields, XModule): css = {'scss': [resource_string(__name__, 'css/foldit/leaderboard.scss')]} def __init__(self, *args, **kwargs): - XModule.__init__(self, *args, **kwargs) """ - Example: """ - - self.due_time = time_to_datetime(self.due) + XModule.__init__(self, *args, **kwargs) + self.due_time = self.due def is_complete(self): """ @@ -102,7 +99,7 @@ class FolditModule(FolditFields, XModule): from foldit.models import Score leaders = [(e['username'], e['score']) for e in Score.get_tops_n(10)] - leaders.sort(key=lambda x: -x[1]) + leaders.sort(key=lambda x:-x[1]) return leaders diff --git a/common/lib/xmodule/xmodule/modulestore/draft.py b/common/lib/xmodule/xmodule/modulestore/draft.py index c16c7403a9..048aea8867 100644 --- a/common/lib/xmodule/xmodule/modulestore/draft.py +++ b/common/lib/xmodule/xmodule/modulestore/draft.py @@ -4,6 +4,7 @@ from . import ModuleStoreBase, Location, namedtuple_to_son from .exceptions import ItemNotFoundError from .inheritance import own_metadata from xmodule.exceptions import InvalidVersionError +from pytz import UTC DRAFT = 'draft' # Things w/ these categories should never be marked as version='draft' @@ -197,7 +198,7 @@ class DraftModuleStore(ModuleStoreBase): """ draft = self.get_item(location) - draft.cms.published_date = datetime.utcnow() + draft.cms.published_date = datetime.now(UTC) draft.cms.published_by = published_by_id super(DraftModuleStore, self).update_item(location, draft._model_data._kvs._data) super(DraftModuleStore, self).update_children(location, draft._model_data._kvs._children) diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 4ea83d7e11..6ab6843216 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -52,7 +52,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): xmlstore: the XMLModuleStore to store the loaded modules in """ - self.unnamed = defaultdict(int) # category -> num of new url_names for that category + self.unnamed = defaultdict(int) # category -> num of new url_names for that category self.used_names = defaultdict(set) # category -> set of used url_names self.org, self.course, self.url_name = course_id.split('/') # cdodge: adding the course_id as passed in for later reference rather than having to recomine the org/course/url_name @@ -124,7 +124,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): else: # TODO (vshnayder): We may want to enable this once course repos are cleaned up. # (or we may want to give up on the requirement for non-state-relevant issues...) - #error_tracker("WARNING: no name specified for module. xml='{0}...'".format(xml[:100])) + # error_tracker("WARNING: no name specified for module. xml='{0}...'".format(xml[:100])) pass # Make sure everything is unique @@ -447,7 +447,7 @@ class XMLModuleStore(ModuleStoreBase): def load_extra_content(self, system, course_descriptor, category, base_dir, course_dir, url_name): self._load_extra_content(system, course_descriptor, category, base_dir, course_dir) - # then look in a override folder based on the course run + # then look in a override folder based on the course run if os.path.isdir(base_dir / url_name): self._load_extra_content(system, course_descriptor, category, base_dir / url_name, course_dir) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py index 7dc8d99451..b5d4e1b676 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py @@ -16,6 +16,7 @@ from .peer_grading_service import PeerGradingService, MockPeerGradingService import controller_query_service from datetime import datetime +from django.utils.timezone import UTC log = logging.getLogger("mitx.courseware") @@ -56,7 +57,7 @@ class OpenEndedChild(object): POST_ASSESSMENT = 'post_assessment' DONE = 'done' - #This is used to tell students where they are at in the module + # This is used to tell students where they are at in the module HUMAN_NAMES = { 'initial': 'Not started', 'assessing': 'In progress', @@ -102,7 +103,7 @@ class OpenEndedChild(object): if system.open_ended_grading_interface: self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system) self.controller_qs = controller_query_service.ControllerQueryService( - system.open_ended_grading_interface,system + system.open_ended_grading_interface, system ) else: self.peer_gs = MockPeerGradingService() @@ -130,7 +131,7 @@ class OpenEndedChild(object): pass def closed(self): - if self.close_date is not None and datetime.utcnow() > self.close_date: + if self.close_date is not None and datetime.now(UTC()) > self.close_date: return True return False @@ -138,13 +139,13 @@ class OpenEndedChild(object): if self.closed(): return True, { 'success': False, - #This is a student_facing_error + # This is a student_facing_error 'error': 'The problem close date has passed, and this problem is now closed.' } elif self.child_attempts > self.max_attempts: return True, { 'success': False, - #This is a student_facing_error + # This is a student_facing_error 'error': 'You have attempted this problem {0} times. You are allowed {1} attempts.'.format( self.child_attempts, self.max_attempts ) @@ -272,7 +273,7 @@ class OpenEndedChild(object): try: return Progress(int(self.get_score()['score']), int(self._max_score)) except Exception as err: - #This is a dev_facing_error + # This is a dev_facing_error log.exception("Got bad progress from open ended child module. Max Score: {0}".format(self._max_score)) return None return None @@ -281,10 +282,10 @@ class OpenEndedChild(object): """ return dict out-of-sync error message, and also log. """ - #This is a dev_facing_error + # This is a dev_facing_error log.warning("Open ended child state out sync. state: %r, get: %r. %s", self.child_state, get, msg) - #This is a student_facing_error + # This is a student_facing_error return {'success': False, 'error': 'The problem state got out-of-sync. Please try reloading the page.'} @@ -391,7 +392,7 @@ class OpenEndedChild(object): """ overall_success = False if not self.accept_file_upload: - #If the question does not accept file uploads, do not do anything + # If the question does not accept file uploads, do not do anything return True, get_data has_file_to_upload, uploaded_to_s3, image_ok, image_tag = self.check_for_image_and_upload(get_data) @@ -399,19 +400,19 @@ class OpenEndedChild(object): get_data['student_answer'] += image_tag overall_success = True elif has_file_to_upload and not uploaded_to_s3 and image_ok: - #In this case, an image was submitted by the student, but the image could not be uploaded to S3. Likely - #a config issue (development vs deployment). For now, just treat this as a "success" + # In this case, an image was submitted by the student, but the image could not be uploaded to S3. Likely + # a config issue (development vs deployment). For now, just treat this as a "success" log.exception("Student AJAX post to combined open ended xmodule indicated that it contained an image, " "but the image was not able to be uploaded to S3. This could indicate a config" "issue with this deployment, but it could also indicate a problem with S3 or with the" "student image itself.") overall_success = True elif not has_file_to_upload: - #If there is no file to upload, probably the student has embedded the link in the answer text + # If there is no file to upload, probably the student has embedded the link in the answer text success, get_data['student_answer'] = self.check_for_url_in_text(get_data['student_answer']) overall_success = success - #log.debug("Has file: {0} Uploaded: {1} Image Ok: {2}".format(has_file_to_upload, uploaded_to_s3, image_ok)) + # log.debug("Has file: {0} Uploaded: {1} Image Ok: {2}".format(has_file_to_upload, uploaded_to_s3, image_ok)) return overall_success, get_data @@ -441,7 +442,7 @@ class OpenEndedChild(object): success = False allowed_to_submit = True response = {} - #This is a student_facing_error + # This is a student_facing_error error_string = ("You need to peer grade {0} more in order to make another submission. " "You have graded {1}, and {2} are required. You have made {3} successful peer grading submissions.") try: @@ -451,17 +452,17 @@ class OpenEndedChild(object): student_sub_count = response['student_sub_count'] success = True except: - #This is a dev_facing_error + # This is a dev_facing_error log.error("Could not contact external open ended graders for location {0} and student {1}".format( self.location_string, student_id)) - #This is a student_facing_error + # This is a student_facing_error error_message = "Could not contact the graders. Please notify course staff." return success, allowed_to_submit, error_message if count_graded >= count_required: return success, allowed_to_submit, "" else: allowed_to_submit = False - #This is a student_facing_error + # This is a student_facing_error error_message = error_string.format(count_required - count_graded, count_graded, count_required, student_sub_count) return success, allowed_to_submit, error_message diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index ccc3e31f51..d0d6ef9242 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -15,6 +15,7 @@ from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService from open_ended_grading_classes import combined_open_ended_rubric +from django.utils.timezone import UTC log = logging.getLogger(__name__) @@ -76,7 +77,7 @@ class PeerGradingModule(PeerGradingFields, XModule): def __init__(self, system, location, descriptor, model_data): XModule.__init__(self, system, location, descriptor, model_data) - #We need to set the location here so the child modules can use it + # We need to set the location here so the child modules can use it system.set('location', location) self.system = system if (self.system.open_ended_grading_interface): @@ -112,7 +113,7 @@ class PeerGradingModule(PeerGradingFields, XModule): if not self.ajax_url.endswith("/"): self.ajax_url = self.ajax_url + "/" - #StringyInteger could return None, so keep this check. + # StringyInteger could return None, so keep this check. if not isinstance(self.max_grade, int): raise TypeError("max_grade needs to be an integer.") @@ -120,7 +121,7 @@ class PeerGradingModule(PeerGradingFields, XModule): return self._closed(self.timeinfo) def _closed(self, timeinfo): - if timeinfo.close_date is not None and datetime.utcnow() > timeinfo.close_date: + if timeinfo.close_date is not None and datetime.now(UTC()) > timeinfo.close_date: return True return False @@ -166,9 +167,9 @@ class PeerGradingModule(PeerGradingFields, XModule): } if dispatch not in handlers: - #This is a dev_facing_error + # This is a dev_facing_error log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch)) - #This is a dev_facing_error + # This is a dev_facing_error return json.dumps({'error': 'Error handling action. Please try again.', 'success': False}) d = handlers[dispatch](get) @@ -187,7 +188,7 @@ class PeerGradingModule(PeerGradingFields, XModule): count_required = response['count_required'] success = True except GradingServiceError: - #This is a dev_facing_error + # This is a dev_facing_error log.exception("Error getting location data from controller for location {0}, student {1}" .format(location, student_id)) @@ -220,7 +221,7 @@ class PeerGradingModule(PeerGradingFields, XModule): count_graded = response['count_graded'] count_required = response['count_required'] if count_required > 0 and count_graded >= count_required: - #Ensures that once a student receives a final score for peer grading, that it does not change. + # Ensures that once a student receives a final score for peer grading, that it does not change. self.student_data_for_location = response if self.weight is not None: @@ -271,10 +272,10 @@ class PeerGradingModule(PeerGradingFields, XModule): response = self.peer_gs.get_next_submission(location, grader_id) return response except GradingServiceError: - #This is a dev_facing_error + # This is a dev_facing_error log.exception("Error getting next submission. server url: {0} location: {1}, grader_id: {2}" .format(self.peer_gs.url, location, grader_id)) - #This is a student_facing_error + # This is a student_facing_error return {'success': False, 'error': EXTERNAL_GRADER_NO_CONTACT_ERROR} @@ -314,13 +315,13 @@ class PeerGradingModule(PeerGradingFields, XModule): score, feedback, submission_key, rubric_scores, submission_flagged) return response except GradingServiceError: - #This is a dev_facing_error + # This is a dev_facing_error log.exception("""Error saving grade to open ended grading service. server url: {0}, location: {1}, submission_id:{2}, submission_key: {3}, score: {4}""" .format(self.peer_gs.url, location, submission_id, submission_key, score) ) - #This is a student_facing_error + # This is a student_facing_error return { 'success': False, 'error': EXTERNAL_GRADER_NO_CONTACT_ERROR @@ -356,10 +357,10 @@ class PeerGradingModule(PeerGradingFields, XModule): response = self.peer_gs.is_student_calibrated(location, grader_id) return response except GradingServiceError: - #This is a dev_facing_error + # This is a dev_facing_error log.exception("Error from open ended grading service. server url: {0}, grader_id: {0}, location: {1}" .format(self.peer_gs.url, grader_id, location)) - #This is a student_facing_error + # This is a student_facing_error return { 'success': False, 'error': EXTERNAL_GRADER_NO_CONTACT_ERROR @@ -401,17 +402,17 @@ class PeerGradingModule(PeerGradingFields, XModule): response = self.peer_gs.show_calibration_essay(location, grader_id) return response except GradingServiceError: - #This is a dev_facing_error + # This is a dev_facing_error log.exception("Error from open ended grading service. server url: {0}, location: {0}" .format(self.peer_gs.url, location)) - #This is a student_facing_error + # This is a student_facing_error return {'success': False, 'error': EXTERNAL_GRADER_NO_CONTACT_ERROR} # if we can't parse the rubric into HTML, except etree.XMLSyntaxError: - #This is a dev_facing_error + # This is a dev_facing_error log.exception("Cannot parse rubric string.") - #This is a student_facing_error + # This is a student_facing_error return {'success': False, 'error': 'Error displaying submission. Please notify course staff.'} @@ -455,11 +456,11 @@ class PeerGradingModule(PeerGradingFields, XModule): response['actual_rubric'] = rubric_renderer.render_rubric(response['actual_rubric'])['html'] return response except GradingServiceError: - #This is a dev_facing_error + # This is a dev_facing_error log.exception( "Error saving calibration grade, location: {0}, submission_key: {1}, grader_id: {2}".format( location, submission_key, grader_id)) - #This is a student_facing_error + # This is a student_facing_error return self._err_response('There was an error saving your score. Please notify course staff.') def peer_grading_closed(self): @@ -491,13 +492,13 @@ class PeerGradingModule(PeerGradingFields, XModule): problem_list = problem_list_dict['problem_list'] except GradingServiceError: - #This is a student_facing_error + # This is a student_facing_error error_text = EXTERNAL_GRADER_NO_CONTACT_ERROR log.error(error_text) success = False # catch error if if the json loads fails except ValueError: - #This is a student_facing_error + # This is a student_facing_error error_text = "Could not get list of problems to peer grade. Please notify course staff." log.error(error_text) success = False @@ -557,8 +558,8 @@ class PeerGradingModule(PeerGradingFields, XModule): ''' if get is None or get.get('location') is None: if self.use_for_single_location not in TRUE_DICT: - #This is an error case, because it must be set to use a single location to be called without get parameters - #This is a dev_facing_error + # This is an error case, because it must be set to use a single location to be called without get parameters + # This is a dev_facing_error log.error( "Peer grading problem in peer_grading_module called with no get parameters, but use_for_single_location is False.") return {'html': "", 'success': False} diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py index 0d789964e9..53181b5a28 100644 --- a/common/lib/xmodule/xmodule/tests/test_course_module.py +++ b/common/lib/xmodule/xmodule/tests/test_course_module.py @@ -1,5 +1,4 @@ import unittest -from time import strptime import datetime from fs.memoryfs import MemoryFS @@ -8,13 +7,13 @@ from mock import Mock, patch from xmodule.modulestore.xml import ImportSystem, XMLModuleStore import xmodule.course_module -from xmodule.util.date_utils import time_to_datetime +from django.utils.timezone import UTC ORG = 'test_org' COURSE = 'test_course' -NOW = strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00') +NOW = datetime.datetime.strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00').replace(tzinfo=UTC()) class DummySystem(ImportSystem): @@ -81,10 +80,10 @@ class IsNewCourseTestCase(unittest.TestCase): Mock(wraps=datetime.datetime) ) mocked_datetime = datetime_patcher.start() - mocked_datetime.utcnow.return_value = time_to_datetime(NOW) + mocked_datetime.now.return_value = NOW self.addCleanup(datetime_patcher.stop) - @patch('xmodule.course_module.time.gmtime') + @patch('xmodule.course_module.datetime.now') def test_sorting_score(self, gmtime_mock): gmtime_mock.return_value = NOW @@ -125,7 +124,7 @@ class IsNewCourseTestCase(unittest.TestCase): print "Comparing %s to %s" % (a, b) assertion(a_score, b_score) - @patch('xmodule.course_module.time.gmtime') + @patch('xmodule.course_module.datetime.now') def test_start_date_text(self, gmtime_mock): gmtime_mock.return_value = NOW diff --git a/common/lib/xmodule/xmodule/tests/test_date_utils.py b/common/lib/xmodule/xmodule/tests/test_date_utils.py deleted file mode 100644 index af96de018f..0000000000 --- a/common/lib/xmodule/xmodule/tests/test_date_utils.py +++ /dev/null @@ -1,35 +0,0 @@ -# Tests for xmodule.util.date_utils - -from nose.tools import assert_equals -from xmodule.util import date_utils -import datetime -import time - - -def test_get_time_struct_display(): - assert_equals("", date_utils.get_time_struct_display(None, "")) - test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0)) - assert_equals("03/12/1992", date_utils.get_time_struct_display(test_time, '%m/%d/%Y')) - assert_equals("15:03", date_utils.get_time_struct_display(test_time, '%H:%M')) - - -def test_get_default_time_display(): - assert_equals("", date_utils.get_default_time_display(None)) - test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0)) - assert_equals( - "Mar 12, 1992 at 15:03 UTC", - date_utils.get_default_time_display(test_time)) - assert_equals( - "Mar 12, 1992 at 15:03 UTC", - date_utils.get_default_time_display(test_time, True)) - assert_equals( - "Mar 12, 1992 at 15:03", - date_utils.get_default_time_display(test_time, False)) - - -def test_time_to_datetime(): - assert_equals(None, date_utils.time_to_datetime(None)) - test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0)) - assert_equals( - datetime.datetime(1992, 3, 12, 15, 3, 30), - date_utils.time_to_datetime(test_time)) diff --git a/common/lib/xmodule/xmodule/tests/test_fields.py b/common/lib/xmodule/xmodule/tests/test_fields.py index 9642f7c595..1b6c86c000 100644 --- a/common/lib/xmodule/xmodule/tests/test_fields.py +++ b/common/lib/xmodule/xmodule/tests/test_fields.py @@ -1,24 +1,15 @@ """Tests for classes defined in fields.py.""" import datetime import unittest -from django.utils.timezone import UTC from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean -import time +from django.utils.timezone import UTC class DateTest(unittest.TestCase): date = Date() - @staticmethod - def struct_to_datetime(struct_time): - return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, - struct_time.tm_mday, struct_time.tm_hour, - struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC()) - - def compare_dates(self, date1, date2, expected_delta): - dt1 = DateTest.struct_to_datetime(date1) - dt2 = DateTest.struct_to_datetime(date2) - self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-" - + str(date2) + "!=" + str(expected_delta)) + def compare_dates(self, dt1, dt2, expected_delta): + self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "-" + + str(dt2) + "!=" + str(expected_delta)) def test_from_json(self): '''Test conversion from iso compatible date strings to struct_time''' @@ -55,10 +46,10 @@ class DateTest(unittest.TestCase): def test_old_due_date_format(self): current = datetime.datetime.today() self.assertEqual( - time.struct_time((current.year, 3, 12, 12, 0, 0, 1, 71, 0)), + datetime.datetime(current.year, 3, 12, 12, tzinfo=UTC()), DateTest.date.from_json("March 12 12:00")) self.assertEqual( - time.struct_time((current.year, 12, 4, 16, 30, 0, 2, 338, 0)), + datetime.datetime(current.year, 12, 4, 16, 30, tzinfo=UTC()), DateTest.date.from_json("December 4 16:30")) def test_to_json(self): @@ -67,7 +58,7 @@ class DateTest(unittest.TestCase): ''' self.assertEqual( DateTest.date.to_json( - time.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ")), + datetime.datetime.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ")), "2012-12-31T23:59:59Z") self.assertEqual( DateTest.date.to_json( @@ -76,7 +67,7 @@ class DateTest(unittest.TestCase): self.assertEqual( DateTest.date.to_json( DateTest.date.from_json("2012-12-31T23:00:01-01:00")), - "2013-01-01T00:00:01Z") + "2012-12-31T23:00:01-01:00") class StringyIntegerTest(unittest.TestCase): diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py index bb0d200bb6..677dd4d80e 100644 --- a/common/lib/xmodule/xmodule/tests/test_import.py +++ b/common/lib/xmodule/xmodule/tests/test_import.py @@ -13,6 +13,8 @@ from xmodule.modulestore.inheritance import compute_inherited_metadata from xmodule.fields import Date from .test_export import DATA_DIR +import datetime +from django.utils.timezone import UTC ORG = 'test_org' COURSE = 'test_course' @@ -40,7 +42,7 @@ class DummySystem(ImportSystem): load_error_modules=load_error_modules, ) - def render_template(self, template, context): + def render_template(self, _template, _context): raise Exception("Shouldn't be called") @@ -62,6 +64,7 @@ class BaseCourseTestCase(unittest.TestCase): class ImportTestCase(BaseCourseTestCase): + date = Date() def test_fallback(self): '''Check that malformed xml loads as an ErrorDescriptor.''' @@ -145,15 +148,18 @@ class ImportTestCase(BaseCourseTestCase): descriptor = system.process_xml(start_xml) compute_inherited_metadata(descriptor) + # pylint: disable=W0212 print(descriptor, descriptor._model_data) - self.assertEqual(descriptor.lms.due, Date().from_json(v)) + self.assertEqual(descriptor.lms.due, ImportTestCase.date.from_json(v)) # Check that the child inherits due correctly child = descriptor.get_children()[0] - self.assertEqual(child.lms.due, Date().from_json(v)) + self.assertEqual(child.lms.due, ImportTestCase.date.from_json(v)) self.assertEqual(child._inheritable_metadata, child._inherited_metadata) self.assertEqual(2, len(child._inherited_metadata)) - self.assertEqual('1970-01-01T00:00:00Z', child._inherited_metadata['start']) + self.assertLessEqual(ImportTestCase.date.from_json( + child._inherited_metadata['start']), + datetime.datetime.now(UTC())) self.assertEqual(v, child._inherited_metadata['due']) # Now export and check things @@ -209,9 +215,13 @@ class ImportTestCase(BaseCourseTestCase): # Check that the child does not inherit a value for due child = descriptor.get_children()[0] self.assertEqual(child.lms.due, None) + # pylint: disable=W0212 self.assertEqual(child._inheritable_metadata, child._inherited_metadata) self.assertEqual(1, len(child._inherited_metadata)) - self.assertEqual('1970-01-01T00:00:00Z', child._inherited_metadata['start']) + # why do these tests look in the internal structure v just calling child.start? + self.assertLessEqual( + ImportTestCase.date.from_json(child._inherited_metadata['start']), + datetime.datetime.now(UTC())) def test_metadata_override_default(self): """ @@ -230,14 +240,17 @@ class ImportTestCase(BaseCourseTestCase):
'''.format(due=course_due, org=ORG, course=COURSE, url_name=url_name) descriptor = system.process_xml(start_xml) child = descriptor.get_children()[0] + # pylint: disable=W0212 child._model_data['due'] = child_due compute_inherited_metadata(descriptor) - self.assertEqual(descriptor.lms.due, Date().from_json(course_due)) - self.assertEqual(child.lms.due, Date().from_json(child_due)) + self.assertEqual(descriptor.lms.due, ImportTestCase.date.from_json(course_due)) + self.assertEqual(child.lms.due, ImportTestCase.date.from_json(child_due)) # Test inherited metadata. Due does not appear here (because explicitly set on child). self.assertEqual(1, len(child._inherited_metadata)) - self.assertEqual('1970-01-01T00:00:00Z', child._inherited_metadata['start']) + self.assertLessEqual( + ImportTestCase.date.from_json(child._inherited_metadata['start']), + datetime.datetime.now(UTC())) # Test inheritable metadata. This has the course inheritable value for due. self.assertEqual(2, len(child._inheritable_metadata)) self.assertEqual(course_due, child._inheritable_metadata['due']) diff --git a/common/lib/xmodule/xmodule/timeinfo.py b/common/lib/xmodule/xmodule/timeinfo.py index a7743b6bee..9a63c0477d 100644 --- a/common/lib/xmodule/xmodule/timeinfo.py +++ b/common/lib/xmodule/xmodule/timeinfo.py @@ -1,5 +1,4 @@ from .timeparse import parse_timedelta -from xmodule.util.date_utils import time_to_datetime import logging log = logging.getLogger(__name__) @@ -17,7 +16,7 @@ class TimeInfo(object): """ def __init__(self, due_date, grace_period_string): if due_date is not None: - self.display_due_date = time_to_datetime(due_date) + self.display_due_date = due_date else: self.display_due_date = None diff --git a/common/lib/xmodule/xmodule/timeparse.py b/common/lib/xmodule/xmodule/timeparse.py index 15a8233ccb..b189262761 100644 --- a/common/lib/xmodule/xmodule/timeparse.py +++ b/common/lib/xmodule/xmodule/timeparse.py @@ -1,9 +1,8 @@ """ Helper functions for handling time in the format we like. """ -import time import re -from datetime import timedelta +from datetime import timedelta, datetime TIME_FORMAT = "%Y-%m-%dT%H:%M" @@ -17,14 +16,14 @@ def parse_time(time_str): Raises ValueError if the string is not in the right format. """ - return time.strptime(time_str, TIME_FORMAT) + return datetime.strptime(time_str, TIME_FORMAT) -def stringify_time(time_struct): +def stringify_time(dt): """ - Convert a time struct to a string + Convert a datetime struct to a string """ - return time.strftime(TIME_FORMAT, time_struct) + return dt.isoformat() def parse_timedelta(time_str): """ diff --git a/common/lib/xmodule/xmodule/util/date_utils.py b/common/lib/xmodule/xmodule/util/date_utils.py index 1e64856e8f..050d65fcf1 100644 --- a/common/lib/xmodule/xmodule/util/date_utils.py +++ b/common/lib/xmodule/xmodule/util/date_utils.py @@ -1,34 +1,20 @@ -import time -import datetime - - -def get_default_time_display(time_struct, show_timezone=True): +def get_default_time_display(dt, show_timezone=True): """ - Converts a time struct to a string representation. This is the default + Converts a datetime to a string representation. This is the default representation used in Studio and LMS. It is of the form "Apr 09, 2013 at 16:00" or "Apr 09, 2013 at 16:00 UTC", depending on the value of show_timezone. - If None is passed in for time_struct, an empty string will be returned. + If None is passed in for dt, an empty string will be returned. The default value of show_timezone is True. """ - timezone = "" if time_struct is None or not show_timezone else " UTC" - return get_time_struct_display(time_struct, "%b %d, %Y at %H:%M") + timezone - - -def get_time_struct_display(time_struct, format): - """ - Converts a time struct to a string based on the given format. - - If None is passed in, an empty string will be returned. - """ - return '' if time_struct is None else time.strftime(format, time_struct) - - -def time_to_datetime(time_struct): - """ - Convert a time struct to a datetime. - - If None is passed in, None will be returned. - """ - return datetime.datetime(*time_struct[:6]) if time_struct else None + timezone = "" + if dt is not None and show_timezone: + if dt.tzinfo is not None: + try: + timezone = dt.tzinfo.tzname(dt) + except NotImplementedError: + timezone = " UTC" + else: + timezone = " UTC" + return dt.strftime("%b %d, %Y at %H:%M") + timezone diff --git a/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml b/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml index 47b19f75ed..35e4704d7c 100644 --- a/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml +++ b/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml @@ -1,24 +1,34 @@ - - - - - - S1E4 has been removed… - - - - + + + + + + S1E4 has been removed… + + + + - - - - -

Inline content…

- -
-
+ + + + +

Inline content…

+ +
+
diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index ace9c0096b..07987a8edf 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -16,6 +16,7 @@ from xmodule.x_module import XModule, XModuleDescriptor from student.models import CourseEnrollmentAllowed from courseware.masquerade import is_masquerading_as_student +from django.utils.timezone import UTC DEBUG_ACCESS = False @@ -133,7 +134,7 @@ def _has_access_course_desc(user, course, action): (staff can always enroll) """ - now = time.gmtime() + now = datetime.now(UTC()) start = course.enrollment_start end = course.enrollment_end @@ -242,7 +243,7 @@ def _has_access_descriptor(user, descriptor, action, course_context=None): # Check start date if descriptor.lms.start is not None: - now = time.gmtime() + now = datetime.now(UTC()) effective_start = _adjust_start_date_for_beta_testers(user, descriptor) if now > effective_start: # after start date, everyone can see it @@ -365,7 +366,7 @@ def _course_org_staff_group_name(location, course_context=None): def group_names_for(role, location, course_context=None): - """Returns the group names for a given role with this location. Plural + """Returns the group names for a given role with this location. Plural because it will return both the name we expect now as well as the legacy group name we support for backwards compatibility. This should not check the DB for existence of a group (like some of its callers do) because that's @@ -483,8 +484,7 @@ def _adjust_start_date_for_beta_testers(user, descriptor): non-None start date. Returns: - A time, in the same format as returned by time.gmtime(). Either the same as - start, or earlier for beta testers. + A datetime. Either the same as start, or earlier for beta testers. NOTE: number of days to adjust should be cached to avoid looking it up thousands of times per query. @@ -505,15 +505,11 @@ def _adjust_start_date_for_beta_testers(user, descriptor): beta_group = course_beta_test_group_name(descriptor.location) if beta_group in user_groups: debug("Adjust start time: user in group %s", beta_group) - # time_structs don't support subtraction, so convert to datetimes, - # subtract, convert back. - # (fun fact: datetime(*a_time_struct[:6]) is the beautiful syntax for - # converting time_structs into datetimes) - start_as_datetime = datetime(*descriptor.lms.start[:6]) + start_as_datetime = descriptor.lms.start delta = timedelta(descriptor.lms.days_early_for_beta) effective = start_as_datetime - delta # ...and back to time_struct - return effective.timetuple() + return effective return descriptor.lms.start @@ -564,7 +560,7 @@ def _has_access_to_location(user, location, access_level, course_context): return True debug("Deny: user not in groups %s", staff_groups) - if access_level == 'instructor' or access_level == 'staff': # instructors get staff privileges + if access_level == 'instructor' or access_level == 'staff': # instructors get staff privileges instructor_groups = group_names_for_instructor(location, course_context) + \ [_course_org_instructor_group_name(location, course_context)] for instructor_group in instructor_groups: diff --git a/lms/djangoapps/courseware/tests/test_access.py b/lms/djangoapps/courseware/tests/test_access.py index c1bb9f203e..34d064971f 100644 --- a/lms/djangoapps/courseware/tests/test_access.py +++ b/lms/djangoapps/courseware/tests/test_access.py @@ -1,18 +1,12 @@ -import unittest -import logging -import time -from mock import Mock, MagicMock, patch +from mock import Mock, patch -from django.conf import settings from django.test import TestCase -from xmodule.course_module import CourseDescriptor -from xmodule.error_module import ErrorDescriptor from xmodule.modulestore import Location -from xmodule.timeparse import parse_time -from xmodule.x_module import XModule, XModuleDescriptor import courseware.access as access from .factories import CourseEnrollmentAllowedFactory +import datetime +from django.utils.timezone import UTC class AccessTestCase(TestCase): @@ -77,7 +71,7 @@ class AccessTestCase(TestCase): # TODO: override DISABLE_START_DATES and test the start date branch of the method u = Mock() d = Mock() - d.start = time.gmtime(time.time() - 86400) # make sure the start time is in the past + d.start = datetime.datetime.now(UTC()) - datetime.timedelta(days=1) # make sure the start time is in the past # Always returns true because DISABLE_START_DATES is set in test.py self.assertTrue(access._has_access_descriptor(u, d, 'load')) @@ -85,8 +79,8 @@ class AccessTestCase(TestCase): def test__has_access_course_desc_can_enroll(self): u = Mock() - yesterday = time.gmtime(time.time() - 86400) - tomorrow = time.gmtime(time.time() + 86400) + yesterday = datetime.datetime.now(UTC()) - datetime.timedelta(days=1) + tomorrow = datetime.datetime.now(UTC()) + datetime.timedelta(days=1) c = Mock(enrollment_start=yesterday, enrollment_end=tomorrow) # User can enroll if it is between the start and end dates diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index ec3e55b1b8..f037fc6c3e 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -3,7 +3,6 @@ Test for lms courseware app ''' import logging import json -import time import random from urlparse import urlsplit, urlunsplit @@ -30,6 +29,8 @@ from xmodule.modulestore.django import modulestore from xmodule.modulestore import Location from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml import XMLModuleStore +import datetime +from django.utils.timezone import UTC log = logging.getLogger("mitx." + __name__) @@ -603,9 +604,9 @@ class TestViewAuth(LoginEnrollmentTestCase): """Actually do the test, relying on settings to be right.""" # Make courses start in the future - tomorrow = time.time() + 24 * 3600 - self.toy.lms.start = time.gmtime(tomorrow) - self.full.lms.start = time.gmtime(tomorrow) + tomorrow = datetime.datetime.now(UTC()) + datetime.timedelta(days=1) + self.toy.lms.start = tomorrow + self.full.lms.start = tomorrow self.assertFalse(self.toy.has_started()) self.assertFalse(self.full.has_started()) @@ -728,18 +729,18 @@ class TestViewAuth(LoginEnrollmentTestCase): """Actually do the test, relying on settings to be right.""" # Make courses start in the future - tomorrow = time.time() + 24 * 3600 - nextday = tomorrow + 24 * 3600 - yesterday = time.time() - 24 * 3600 + tomorrow = datetime.datetime.now(UTC()) + datetime.timedelta(days=1) + nextday = tomorrow + datetime.timedelta(days=1) + yesterday = datetime.datetime.now(UTC()) - datetime.timedelta(days=1) print "changing" # toy course's enrollment period hasn't started - self.toy.enrollment_start = time.gmtime(tomorrow) - self.toy.enrollment_end = time.gmtime(nextday) + self.toy.enrollment_start = tomorrow + self.toy.enrollment_end = nextday # full course's has - self.full.enrollment_start = time.gmtime(yesterday) - self.full.enrollment_end = time.gmtime(tomorrow) + self.full.enrollment_start = yesterday + self.full.enrollment_end = tomorrow print "login" # First, try with an enrolled student @@ -778,12 +779,10 @@ class TestViewAuth(LoginEnrollmentTestCase): self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) # Make courses start in the future - tomorrow = time.time() + 24 * 3600 - # nextday = tomorrow + 24 * 3600 - # yesterday = time.time() - 24 * 3600 + tomorrow = datetime.datetime.now(UTC()) + datetime.timedelta(days=1) # toy course's hasn't started - self.toy.lms.start = time.gmtime(tomorrow) + self.toy.lms.start = tomorrow self.assertFalse(self.toy.has_started()) # but should be accessible for beta testers @@ -854,7 +853,7 @@ class TestSubmittingProblems(LoginEnrollmentTestCase): modx_url = self.modx_url(problem_location, 'problem_check') answer_key_prefix = 'input_i4x-edX-{}-problem-{}_'.format(self.course_slug, problem_url_name) resp = self.client.post(modx_url, - { (answer_key_prefix + k): v for k,v in responses.items() } + { (answer_key_prefix + k): v for k, v in responses.items() } ) return resp @@ -925,7 +924,7 @@ class TestCourseGrader(TestSubmittingProblems): # Only get half of the first problem correct self.submit_question_answer('H1P1', {'2_1': 'Correct', '2_2': 'Incorrect'}) self.check_grade_percent(0.06) - self.assertEqual(earned_hw_scores(), [1.0, 0, 0]) # Order matters + self.assertEqual(earned_hw_scores(), [1.0, 0, 0]) # Order matters self.assertEqual(score_for_hw('Homework1'), [1.0, 0.0]) # Get both parts of the first problem correct @@ -958,16 +957,16 @@ class TestCourseGrader(TestSubmittingProblems): # Third homework self.submit_question_answer('H3P1', {'2_1': 'Correct', '2_2': 'Correct'}) - self.check_grade_percent(0.42) # Score didn't change + self.check_grade_percent(0.42) # Score didn't change self.assertEqual(earned_hw_scores(), [4.0, 4.0, 2.0]) self.submit_question_answer('H3P2', {'2_1': 'Correct', '2_2': 'Correct'}) - self.check_grade_percent(0.5) # Now homework2 dropped. Score changes + self.check_grade_percent(0.5) # Now homework2 dropped. Score changes self.assertEqual(earned_hw_scores(), [4.0, 4.0, 4.0]) # Now we answer the final question (worth half of the grade) self.submit_question_answer('FinalQuestion', {'2_1': 'Correct', '2_2': 'Correct'}) - self.check_grade_percent(1.0) # Hooray! We got 100% + self.check_grade_percent(1.0) # Hooray! We got 100% @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) @@ -1000,7 +999,7 @@ class TestSchematicResponse(TestSubmittingProblems): { '2_1': json.dumps( [['transient', {'Z': [ [0.0000004, 2.8], - [0.0000009, 0.0], # wrong. + [0.0000009, 0.0], # wrong. [0.0000014, 2.8], [0.0000019, 2.8], [0.0000024, 2.8], diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 276956f0e9..007a8fedfd 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -1,14 +1,9 @@ -import time from collections import defaultdict import logging -import time import urllib from datetime import datetime from courseware.module_render import get_module -from xmodule.modulestore import Location -from xmodule.modulestore.django import modulestore -from xmodule.modulestore.search import path_to_location from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.db import connection @@ -16,13 +11,12 @@ from django.http import HttpResponse from django.utils import simplejson from django_comment_common.models import Role from django_comment_client.permissions import check_permissions_by_view -from xmodule.modulestore.exceptions import NoPathToItem from mitxmako import middleware import pystache_custom as pystache -from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore +from django.utils.timezone import UTC log = logging.getLogger(__name__) @@ -100,7 +94,7 @@ def get_discussion_category_map(course): def filter_unstarted_categories(category_map): - now = time.gmtime() + now = datetime.now(UTC()) result_map = {} @@ -220,7 +214,7 @@ def initialize_discussion_info(course): for topic, entry in course.discussion_topics.items(): category_map['entries'][topic] = {"id": entry["id"], "sort_key": entry.get("sort_key", topic), - "start_date": time.gmtime()} + "start_date": datetime.now(UTC())} sort_map_entries(category_map) _DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map @@ -292,7 +286,7 @@ def get_ability(course_id, content, user): 'can_vote': check_permissions_by_view(user, course_id, content, "vote_for_thread" if content['type'] == 'thread' else "vote_for_comment"), } -#TODO: RENAME +# TODO: RENAME def get_annotated_content_info(course_id, content, user, user_info): @@ -310,7 +304,7 @@ def get_annotated_content_info(course_id, content, user, user_info): 'ability': get_ability(course_id, content, user), } -#TODO: RENAME +# TODO: RENAME def get_annotated_content_infos(course_id, thread, user, user_info): From b7cfbe0ce61f3350a74b82009a73ca7b48a03f5c Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Wed, 29 May 2013 11:40:53 -0400 Subject: [PATCH 025/173] Add safety check for start dates unbound --- cms/templates/edit_subsection.html | 4 ++-- cms/templates/overview.html | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index 4aae070ca1..cbce91ab44 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -37,13 +37,13 @@
diff --git a/cms/templates/overview.html b/cms/templates/overview.html index 0b82d76943..43d0afc263 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -154,8 +154,12 @@