Commit d8f307a5 by Peter Fogg

Merge branch 'master' into peter-fogg/remove-video-xml

parents e0045499 1ef29053
......@@ -75,3 +75,4 @@ Frances Botsford <frances@edx.org>
Jonah Stanley <Jonah_Stanley@brown.edu>
Slater Victoroff <slater.r.victoroff@gmail.com>
Peter Fogg <peter.p.fogg@gmail.com>
Renzo Lucioni <renzolucioni@gmail.com>
\ No newline at end of file
Change Log
----------
These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
Studio, LMS: Make ModelTypes more strict about their expected content (for
instance, Boolean, Integer, String), but also allow them to hold either the
typed value, or a String that can be converted to their typed value. For example,
an Integer can contain 3 or '3'. This changed an update to the xblock library.
LMS: Some errors handling Non-ASCII data in XML courses have been fixed.
LMS: Add page-load tracking using segment-io (if SEGMENT_IO_LMS_KEY and
SEGMENT_IO_LMS feature flag is on)
Blades: Simplify calc.py (which is used for the Numerical/Formula responses); add trig/other functions.
LMS: Background colors on login, register, and courseware have been corrected
back to white.
LMS: Accessibility improvements have been made to several courseware and
navigation elements.
LMS: Small design/presentation changes to login and register views.
LMS: Functionality added to instructor enrollment tab in LMS such that invited
student can be auto-enrolled in course or when activating if not current
student.
Blades: Staff debug info is now accessible for Graphical Slider Tool problems.
Blades: For Video Alpha the events ready, play, pause, seek, and speed change
are logged on the server (in the logs).
Common: Developers can now have private Django settings files.
Common: Safety code added to prevent anything above the vertical level in the
course tree from being marked as version='draft'. It will raise an exception if
the code tries to so mark a node. We need the backtraces to figure out where
this very infrequent intermittent marking was occurring. It was making courses
look different in Studio than in LMS.
Deploy: MKTG_URLS is now read from env.json.
Common: Theming makes it possible to change the look of the site, from
Stanford.
Common: Accessibility UI fixes.
Common: The "duplicate email" error message is more informative.
Studio: Component metadata settings editor.
Studio: Autoplay is disabled (only in Studio).
Studio: Single-click creation for video and discussion components.
Studio: fixed a bad link in the activation page.
LMS: Changed the help button text.
LMS: Fixed failing numeric response (decimal but no trailing digits).
LMS: XML Error module no longer shows students a stack trace.
Blades: Videoalpha.
XModules: Added partial credit for foldit module.
XModules: Added "randomize" XModule to list of XModule types.
XModules: Show errors with full descriptors.
XQueue: Fixed (hopefully) worker crash when the connection to RabbitMQ is
dropped suddenly.
XQueue: Upload file submissions to a specially named bucket in S3.
Common: Removed request debugger.
Common: Updated Django to version 1.4.5.
Common: Updated CodeJail.
Common: Allow setting of authentication session cookie name.
......@@ -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]"`.
......
......@@ -28,11 +28,18 @@ Feature: Advanced (manual) course policy
Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio
When I create a JSON object as a value
When I create a JSON object as a value for "discussion_topics"
Then it is displayed as formatted
And I reload the page
Then it is displayed as formatted
Scenario: Test error if value supplied is of the wrong type
Given I am on the Advanced Course Settings page in Studio
When I create a JSON object as a value for "display_name"
Then I get an error on save
And I reload the page
Then the policy key value is unchanged
Scenario: Test automatic quoting of non-JSON values
Given I am on the Advanced Course Settings page in Studio
When I create a non-JSON value not in quotes
......
......@@ -2,8 +2,7 @@
#pylint: disable=W0621
from lettuce import world, step
from common import *
from nose.tools import assert_false, assert_equal
from nose.tools import assert_false, assert_equal, assert_regexp_matches
"""
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html
......@@ -52,9 +51,9 @@ def edit_the_value_of_a_policy_key_and_save(step):
change_display_name_value(step, '"foo"')
@step('I create a JSON object as a value$')
def create_JSON_object(step):
change_display_name_value(step, '{"key": "value", "key_2": "value_2"}')
@step('I create a JSON object as a value for "(.*)"$')
def create_JSON_object(step, key):
change_value(step, key, '{"key": "value", "key_2": "value_2"}')
@step('I create a non-JSON value not in quotes$')
......@@ -82,7 +81,12 @@ def they_are_alphabetized(step):
@step('it is displayed as formatted$')
def it_is_formatted(step):
assert_policy_entries([DISPLAY_NAME_KEY], ['{\n "key": "value",\n "key_2": "value_2"\n}'])
assert_policy_entries(['discussion_topics'], ['{\n "key": "value",\n "key_2": "value_2"\n}'])
@step('I get an error on save$')
def error_on_save(step):
assert_regexp_matches(world.css_text('#notification-error-description'), 'Incorrect setting format')
@step('it is displayed as a string')
......@@ -124,11 +128,16 @@ def get_display_name_value():
def change_display_name_value(step, new_value):
change_value(step, DISPLAY_NAME_KEY, new_value)
world.css_find(".CodeMirror")[get_index_of(DISPLAY_NAME_KEY)].click()
def change_value(step, key, new_value):
index = get_index_of(key)
world.css_find(".CodeMirror")[index].click()
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
display_name = get_display_name_value()
for count in range(len(display_name)):
current_value = world.css_find(VALUE_CSS)[index].value
g._element.send_keys(Keys.CONTROL + Keys.END)
for count in range(len(current_value)):
g._element.send_keys(Keys.END, Keys.BACK_SPACE)
# Must delete "" before typing the JSON value
g._element.send_keys(Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
......
......@@ -41,7 +41,9 @@ def i_see_five_settings_with_values(step):
@step('I can modify the display name')
def i_can_modify_the_display_name(step):
world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill('modified')
# Verifying that the display name can be a string containing a floating point value
# (to confirm that we don't throw an error because it is of the wrong type).
world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill('3.4')
verify_modified_display_name()
......@@ -172,7 +174,7 @@ def verify_modified_randomization():
def verify_modified_display_name():
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, 'modified', True)
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, '3.4', True)
def verify_modified_display_name_with_special_chars():
......
......@@ -19,6 +19,24 @@ class ChecklistTestCase(CourseTestCase):
modulestore = get_modulestore(self.course.location)
return modulestore.get_item(self.course.location).checklists
def compare_checklists(self, persisted, request):
"""
Handles url expansion as possible difference and descends into guts
:param persisted:
:param request:
"""
self.assertEqual(persisted['short_description'], request['short_description'])
compare_urls = (persisted.get('action_urls_expanded') == request.get('action_urls_expanded'))
for pers, req in zip(persisted['items'], request['items']):
self.assertEqual(pers['short_description'], req['short_description'])
self.assertEqual(pers['long_description'], req['long_description'])
self.assertEqual(pers['is_checked'], req['is_checked'])
if compare_urls:
self.assertEqual(pers['action_url'], req['action_url'])
self.assertEqual(pers['action_text'], req['action_text'])
self.assertEqual(pers['action_external'], req['action_external'])
def test_get_checklists(self):
""" Tests the get checklists method. """
checklists_url = get_url_reverse('Checklists', self.course)
......@@ -31,9 +49,9 @@ class ChecklistTestCase(CourseTestCase):
self.course.checklists = None
modulestore = get_modulestore(self.course.location)
modulestore.update_metadata(self.course.location, own_metadata(self.course))
self.assertEquals(self.get_persisted_checklists(), None)
self.assertEqual(self.get_persisted_checklists(), None)
response = self.client.get(checklists_url)
self.assertEquals(payload, response.content)
self.assertEqual(payload, response.content)
def test_update_checklists_no_index(self):
""" No checklist index, should return all of them. """
......@@ -43,7 +61,8 @@ class ChecklistTestCase(CourseTestCase):
'name': self.course.location.name})
returned_checklists = json.loads(self.client.get(update_url).content)
self.assertListEqual(self.get_persisted_checklists(), returned_checklists)
for pay, resp in zip(self.get_persisted_checklists(), returned_checklists):
self.compare_checklists(pay, resp)
def test_update_checklists_index_ignored_on_get(self):
""" Checklist index ignored on get. """
......@@ -53,7 +72,8 @@ class ChecklistTestCase(CourseTestCase):
'checklist_index': 1})
returned_checklists = json.loads(self.client.get(update_url).content)
self.assertListEqual(self.get_persisted_checklists(), returned_checklists)
for pay, resp in zip(self.get_persisted_checklists(), returned_checklists):
self.compare_checklists(pay, resp)
def test_update_checklists_post_no_index(self):
""" No checklist index, will error on post. """
......@@ -78,13 +98,18 @@ class ChecklistTestCase(CourseTestCase):
'course': self.course.location.course,
'name': self.course.location.name,
'checklist_index': 2})
def get_first_item(checklist):
return checklist['items'][0]
payload = self.course.checklists[2]
self.assertFalse(payload.get('is_checked'))
payload['is_checked'] = True
self.assertFalse(get_first_item(payload).get('is_checked'))
get_first_item(payload)['is_checked'] = True
returned_checklist = json.loads(self.client.post(update_url, json.dumps(payload), "application/json").content)
self.assertTrue(returned_checklist.get('is_checked'))
self.assertEqual(self.get_persisted_checklists()[2], returned_checklist)
self.assertTrue(get_first_item(returned_checklist).get('is_checked'))
pers = self.get_persisted_checklists()
self.compare_checklists(pers[2], returned_checklist)
def test_update_checklists_delete_unsupported(self):
""" Delete operation is not supported. """
......@@ -93,4 +118,4 @@ class ChecklistTestCase(CourseTestCase):
'name': self.course.location.name,
'checklist_index': 100})
response = self.client.delete(update_url)
self.assertContains(response, 'Unsupported request', status_code=400)
\ No newline at end of file
self.assertContains(response, 'Unsupported request', status_code=400)
......@@ -402,8 +402,11 @@ def course_advanced_updates(request, org, course, name):
request_body.update({'tabs': new_tabs})
# Indicate that tabs should *not* be filtered out of the metadata
filter_tabs = False
response_json = json.dumps(CourseMetadata.update_from_json(location,
try:
response_json = json.dumps(CourseMetadata.update_from_json(location,
request_body,
filter_tabs=filter_tabs))
except (TypeError, ValueError), e:
return HttpResponseBadRequest("Incorrect setting format. " + str(e), content_type="text/plain")
return HttpResponse(response_json, mimetype="application/json")
......@@ -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)
......@@ -5,6 +5,7 @@ from django.contrib.auth.models import Group
from datetime import datetime
from factory import DjangoModelFactory, SubFactory, PostGenerationMethodCall, post_generation, Sequence
from uuid import uuid4
from pytz import UTC
# Factories don't have __init__ methods, and are self documenting
# pylint: disable=W0232
......@@ -46,8 +47,8 @@ class UserFactory(DjangoModelFactory):
is_staff = False
is_active = True
is_superuser = False
last_login = datetime(2012, 1, 1)
date_joined = datetime(2011, 1, 1)
last_login = datetime(2012, 1, 1, tzinfo=UTC)
date_joined = datetime(2011, 1, 1, tzinfo=UTC)
@post_generation
def profile(obj, create, extracted, **kwargs):
......
"""
Parser and evaluator for FormulaResponse and NumericalResponse
Uses pyparsing to parse. Main function as of now is evaluator().
"""
import copy
import logging
import math
import operator
import re
import numpy
import numbers
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
numpy.seterr(all='ignore') # Also: 'ignore', 'warn' (default), 'raise'
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,
ZeroOrMore, MatchFirst,
Optional, Forward,
CaselessLiteral,
stringEnd, Suppress, Combine)
default_functions = {'sin': numpy.sin,
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),
DEFAULT_VARIABLES = {'i': numpy.complex(0, 1),
'j': numpy.complex(0, 1),
'e': numpy.e,
'pi': numpy.pi,
'k': scipy.constants.k,
......@@ -37,65 +66,166 @@ default_variables = {'j': numpy.complex(0, 1),
'q': scipy.constants.e
}
log = logging.getLogger("mitx.courseware.capa")
# We eliminated the following extreme suffixes:
# P (1e15), E (1e18), Z (1e21), Y (1e24),
# f (1e-15), a (1e-18), z (1e-21), y (1e-24)
# 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,
'c': 1e-2, 'm': 1e-3, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12}
class UndefinedVariable(Exception):
def raiseself(self):
''' Helper so we can use inside of a lambda '''
raise self
general_whitespace = re.compile('[^\w]+')
"""
Used to indicate the student input of a variable, which was unused by the
instructor.
"""
pass
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.
Otherwise, raise an UndefinedVariable containing all bad variables.
achar = reduce(lambda a,b:a|b ,map(Literal,alphas)) # Any alphabetic character
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:
if len(v) == 0:
Pyparsing uses a left-to-right parser, which makes a more
elegant approach pretty hopeless.
"""
general_whitespace = re.compile('[^\\w]+')
# List of all alnums in string
possible_variables = re.split(general_whitespace, string)
bad_variables = []
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].isdigit(): # 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(input_dict):
"""
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 {k.lower(): v for k, v in input_dict.iteritems()}
# 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
"""
if text[-1] in SUFFIXES:
return float(text[:-1]) * SUFFIXES[text[-1]]
else:
return float(text)
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(parse_result))
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
"""
# pyparsing.ParseResults doesn't play well with reverse()
parse_result = reversed(parse_result)
# the result of an exponentiation is called a power
power = reduce(lambda a, b: b ** a, parse_result)
return power
def parallel(parse_result):
"""
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
"""
# 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')
reciprocals = [1. / e for e in parse_result]
return 1. / sum(reciprocals)
def sum_parse_action(parse_result):
"""
Add the inputs
[ 1, '+', 2, '-', 3 ] -> 0
Allow a leading + or -
"""
total = 0.0
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 = current_op(total, token)
return total
def prod_parse_action(parse_result):
"""
Multiply the inputs
[ 1, '*', 2, '/', 3 ] => 0.66
"""
prod = 1.0
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 = current_op(prod, token)
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))
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)
if not cs:
all_variables = lower_dict(all_variables)
all_functions = lower_dict(all_functions)
"""
all_variables = copy.copy(DEFAULT_VARIABLES)
all_functions = copy.copy(DEFAULT_FUNCTIONS)
all_variables.update(variables)
all_functions.update(functions)
......@@ -113,122 +243,59 @@ 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, ".-+*/()^")
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)
# by default pyparsing allows spaces between tokens--Combine prevents that
inner_number = Combine(inner_number)
# 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
number.setParseAction(number_parse_action) # Convert to number
# Predefine recursive variables
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))
else:
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 = sreduce(lambda x, y: x | y,
map(lambda x: CasedLiteral(x), all_functions.keys()))
function = funcnames + lpar.suppress() + expr + rpar.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
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)
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(")")
# Do the following in the correct order to preserve order of operation
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]
"""
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)
......@@ -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
......
......@@ -1738,6 +1738,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
......
......@@ -6,7 +6,7 @@ from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlDescriptor
from xmodule.exceptions import InvalidDefinitionError
from xblock.core import String, Scope, Object
from xblock.core import String, Scope, Dict
DEFAULT = "_DEFAULT_GROUP"
......@@ -32,9 +32,9 @@ def group_from_value(groups, v):
class ABTestFields(object):
group_portions = Object(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content)
group_assignments = Object(help="What group this user belongs to", scope=Scope.preferences, default={})
group_content = Object(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
group_portions = Dict(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content)
group_assignments = Dict(help="What group this user belongs to", scope=Scope.preferences, default={})
group_content = Dict(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content)
has_children = True
......
......@@ -125,6 +125,5 @@ class AnnotatableModule(AnnotatableFields, XModule):
class AnnotatableDescriptor(AnnotatableFields, RawDescriptor):
module_class = AnnotatableModule
stores_state = True
template_dir_name = "annotatable"
mako_template = "widgets/raw-edit.html"
......@@ -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, Dict, Integer, Float
from .fields import Timedelta, Date
from django.utils.timezone import UTC
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": 0}, scope=Scope.settings
......@@ -95,12 +95,12 @@ class CapaFields(object):
{"display_name": "Per Student", "value": "per_student"}]
)
data = String(help="XML data for the problem", scope=Scope.content)
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={})
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)
correct_map = Dict(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={})
input_state = Dict(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state)
student_answers = Dict(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)
......@@ -902,7 +902,6 @@ class CapaDescriptor(CapaFields, RawDescriptor):
module_class = CapaModule
stores_state = True
has_score = True
template_dir_name = 'problem'
mako_template = "widgets/problem-edit.html"
......
......@@ -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"}
......@@ -239,7 +239,6 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
mako_template = "widgets/open-ended-edit.html"
module_class = CombinedOpenEndedModule
stores_state = True
has_score = True
always_recalculate_grades = True
template_dir_name = "combinedopenended"
......
......@@ -92,7 +92,7 @@ class ConditionalModule(ConditionalFields, XModule):
if xml_value and self.required_modules:
for module in self.required_modules:
if not hasattr(module, attr_name):
# We don't throw an exception here because it is possible for
# We don't throw an exception here because it is possible for
# the descriptor of a required module to have a property but
# for the resulting module to be a (flavor of) ErrorModule.
# So just log and return false.
......@@ -161,7 +161,6 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor):
filename_extension = "xml"
stores_state = True
has_score = False
@staticmethod
......
......@@ -15,7 +15,7 @@ from xmodule.util.decorators import lazyproperty
from xmodule.graders import grader_from_conf
import json
from xblock.core import Scope, List, String, Object, Boolean
from xblock.core import Scope, List, String, Dict, Boolean
from .fields import Date
from django.utils.timezone import UTC
from xmodule.util import date_utils
......@@ -154,25 +154,25 @@ class CourseFields(object):
start = Date(help="Start time when this module is visible", scope=Scope.settings)
end = Date(help="Date that this class ends", scope=Scope.settings)
advertised_start = String(help="Date that this course is advertised to start", scope=Scope.settings)
grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content)
grading_policy = Dict(help="Grading policy definition for this class", scope=Scope.content)
show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings)
display_name = String(help="Display name for this module", scope=Scope.settings)
tabs = List(help="List of tabs to enable in this course", scope=Scope.settings)
end_of_course_survey_url = String(help="Url for the end-of-course survey", scope=Scope.settings)
discussion_blackouts = List(help="List of pairs of start/end dates for discussion blackouts", scope=Scope.settings)
discussion_topics = Object(
discussion_topics = Dict(
help="Map of topics names to ids",
scope=Scope.settings
)
testcenter_info = Object(help="Dictionary of Test Center info", scope=Scope.settings)
testcenter_info = Dict(help="Dictionary of Test Center info", scope=Scope.settings)
announcement = Date(help="Date this course is announced", scope=Scope.settings)
cohort_config = Object(help="Dictionary defining cohort configuration", scope=Scope.settings)
cohort_config = Dict(help="Dictionary defining cohort configuration", scope=Scope.settings)
is_new = Boolean(help="Whether this course should be flagged as new", scope=Scope.settings)
no_grade = Boolean(help="True if this course isn't graded", default=False, scope=Scope.settings)
disable_progress_graph = Boolean(help="True if this course shouldn't display the progress graph", default=False, scope=Scope.settings)
pdf_textbooks = List(help="List of dictionaries containing pdf_textbook configuration", scope=Scope.settings)
html_textbooks = List(help="List of dictionaries containing html_textbook configuration", scope=Scope.settings)
remote_gradebook = Object(scope=Scope.settings)
remote_gradebook = Dict(scope=Scope.settings)
allow_anonymous = Boolean(scope=Scope.settings, default=True)
allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False)
advanced_modules = List(help="Beta modules used in your course", scope=Scope.settings)
......
......@@ -6,7 +6,6 @@ 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__)
......@@ -93,42 +92,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 Exception:
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
......@@ -183,7 +183,6 @@ class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
module_class = FolditModule
filename_extension = "xml"
stores_state = True
has_score = True
template_dir_name = "foldit"
......
......@@ -140,12 +140,16 @@ class @VideoCaptionAlpha extends SubviewAlpha
hideCaptions: (hide_captions) =>
if hide_captions
type = 'hide_transcript'
@$('.hide-subtitles').attr('title', 'Turn on captions')
@el.addClass('closed')
else
type = 'show_transcript'
@$('.hide-subtitles').attr('title', 'Turn off captions')
@el.removeClass('closed')
@scrollCaption()
@video.log type,
currentTime: @player.currentTime
$.cookie('hide_captions', hide_captions, expires: 3650, path: '/')
captionHeight: ->
......
......@@ -45,6 +45,8 @@ class @VideoPlayerAlpha extends SubviewAlpha
if @video.show_captions is true
@caption = new VideoCaptionAlpha
el: @el
video: @video
player: @
youtubeId: @video.youtubeId('1.0')
currentSpeed: @currentSpeed()
captionAssetPath: @video.caption_asset_path
......@@ -66,7 +68,16 @@ class @VideoPlayerAlpha extends SubviewAlpha
if @video.end
# work in AS3, not HMLT5. but iframe use AS3
@playerVars.end = @video.end
# There is a bug which prevents YouTube API to correctly set the speed to 1.0 from another speed
# in Firefox when in HTML5 mode. There is a fix which basically reloads the video at speed 1.0
# when this change is requested (instead of simply requesting a speed change to 1.0). This has to
# be done only when the video is being watched in Firefox. We need to figure out what browser is
# currently executing this code.
@video.isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1
if @video.videoType is 'html5'
@video.playerType = 'browser'
@player = new HTML5Video.Player @video.el,
playerVars: @playerVars,
videoSources: @video.html5Sources,
......@@ -79,6 +90,7 @@ class @VideoPlayerAlpha extends SubviewAlpha
youTubeId = @video.videos['1.0']
else
youTubeId = @video.youtubeId()
@video.playerType = 'youtube'
@player = new YT.Player @video.id,
playerVars: @playerVars
videoId: youTubeId
......@@ -99,7 +111,7 @@ class @VideoPlayerAlpha extends SubviewAlpha
@video.log 'load_video'
if @video.videoType is 'html5'
@player.setPlaybackRate @video.speed
unless onTouchBasedDevice()
if not onTouchBasedDevice() and $('.video:first').data('autoplay') is 'True'
$('.video-load-complete:first').data('video').player.play()
onStateChange: (event) =>
......@@ -235,13 +247,18 @@ class @VideoPlayerAlpha extends SubviewAlpha
if @video.videoType is 'youtube'
if @video.show_captions is true
@caption.currentSpeed = newSpeed
if @video.videoType is 'html5'
@player.setPlaybackRate newSpeed
else if @video.videoType is 'youtube'
# We request the reloading of the video in the case when YouTube is in Flash player mode,
# or when we are in Firefox, and the new speed is 1.0. The second case is necessary to
# avoid the bug where in Firefox speed switching to 1.0 in HTML5 player mode is handled
# incorrectly by YouTube API.
if (@video.videoType is 'youtube') or ((@video.isFirefox) and (@video.playerType is 'youtube') and (newSpeed is '1.0'))
if @isPlaying()
@player.loadVideoById(@video.youtubeId(), @currentTime)
else
@player.cueVideoById(@video.youtubeId(), @currentTime)
else if @video.videoType is 'html5'
@player.setPlaybackRate newSpeed
if @video.videoType is 'youtube'
@updatePlayTime @currentTime
......@@ -262,11 +279,15 @@ class @VideoPlayerAlpha extends SubviewAlpha
toggleFullScreen: (event) =>
event.preventDefault()
if @el.hasClass('fullscreen')
type = 'not_fullscreen'
@$('.add-fullscreen').attr('title', 'Fill browser')
@el.removeClass('fullscreen')
else
type = 'fullscreen'
@el.addClass('fullscreen')
@$('.add-fullscreen').attr('title', 'Exit fill browser')
@video.log type,
currentTime: @currentTime
if @video.show_captions is true
@caption.resize()
......
......@@ -823,7 +823,6 @@ class CombinedOpenEndedV1Descriptor():
module_class = CombinedOpenEndedV1Module
filename_extension = "xml"
stores_state = True
has_score = True
template_dir_name = "combinedopenended"
......
......@@ -731,7 +731,6 @@ class OpenEndedDescriptor():
module_class = OpenEndedModule
filename_extension = "xml"
stores_state = True
has_score = True
template_dir_name = "openended"
......
......@@ -286,7 +286,6 @@ class SelfAssessmentDescriptor():
module_class = SelfAssessmentModule
filename_extension = "xml"
stores_state = True
has_score = True
template_dir_name = "selfassessment"
......
......@@ -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 Dict, 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
......@@ -21,7 +21,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
......@@ -29,7 +28,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.',
......@@ -40,22 +39,22 @@ 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}
)
student_data_for_location = Object(
student_data_for_location = Dict(
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"}
......@@ -85,7 +84,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:
......@@ -113,7 +112,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.
# Integer could return None, so keep this check.
if not isinstance(self.max_grade, int):
raise TypeError("max_grade needs to be an integer.")
......@@ -147,7 +146,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']
......@@ -204,7 +203,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:
......@@ -239,7 +238,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
......@@ -557,7 +556,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(
......@@ -603,7 +602,6 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
module_class = PeerGradingModule
filename_extension = "xml"
stores_state = True
has_score = True
always_recalculate_grades = True
template_dir_name = "peer_grading"
......
......@@ -19,7 +19,7 @@ from xmodule.x_module import XModule
from xmodule.stringify import stringify_children
from xmodule.mako_module import MakoModuleDescriptor
from xmodule.xml_module import XmlDescriptor
from xblock.core import Scope, String, Object, Boolean, List
from xblock.core import Scope, String, Dict, Boolean, List
log = logging.getLogger(__name__)
......@@ -30,7 +30,7 @@ class PollFields(object):
voted = Boolean(help="Whether this student has voted on the poll", scope=Scope.user_state, default=False)
poll_answer = String(help="Student answer", scope=Scope.user_state, default='')
poll_answers = Object(help="All possible answers for the poll fro other students", scope=Scope.content)
poll_answers = Dict(help="All possible answers for the poll fro other students", scope=Scope.content)
answers = List(help="Poll answers from xml", scope=Scope.content, default=[])
question = String(help="Poll question", scope=Scope.content, default='')
......@@ -141,7 +141,6 @@ class PollDescriptor(PollFields, MakoModuleDescriptor, XmlDescriptor):
module_class = PollModule
template_dir_name = 'poll'
stores_state = True
@classmethod
def definition_from_xml(cls, xml_object, system):
......
......@@ -94,7 +94,6 @@ class RandomizeDescriptor(RandomizeFields, SequenceDescriptor):
filename_extension = "xml"
stores_state = True
def definition_to_xml(self, resource_fs):
......
......@@ -121,8 +121,6 @@ class SequenceDescriptor(SequenceFields, MakoModuleDescriptor, XmlDescriptor):
mako_template = 'widgets/sequence-edit.html'
module_class = SequenceModule
stores_state = True # For remembering where in the sequence the student is
js = {'coffee': [resource_string(__name__, 'js/src/sequence/edit.coffee')]}
js_module_name = "SequenceDescriptor"
......
"""Tests for classes defined in fields.py."""
import datetime
import unittest
from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean
from django.utils.timezone import UTC
from xmodule.fields import Date, Timedelta
class DateTest(unittest.TestCase):
date = Date()
......@@ -70,54 +71,22 @@ class DateTest(unittest.TestCase):
"2012-12-31T23:00:01-01:00")
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 TimedeltaTest(unittest.TestCase):
delta = Timedelta()
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_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)
)
def test_pass_through(self):
self.assertEquals(123, 123)
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))
)
......@@ -2,11 +2,12 @@
#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 xmodule.xml_module import XmlDescriptor
from xblock.core import Scope, String, Dict, Boolean, Integer, Float, Any, List
from xmodule.fields import Date, Timedelta
from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field
import unittest
from .import test_system
from nose.tools import assert_equals
from mock import Mock
......@@ -17,11 +18,11 @@ 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.
student_answers = Object(scope=Scope.user_state)
student_answers = Dict(scope=Scope.user_state)
# Will be returned, and can override the inherited value from XModule.
display_name = String(scope=Scope.settings, default='local default', display_name='Local Display Name',
help='local help')
......@@ -33,9 +34,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)
......@@ -104,7 +105,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
def test_type_and_options(self):
# test_display_name_field verifies that a String field is of type "Generic".
# test_integer_field verifies that a StringyInteger field is of type "Integer".
# test_integer_field verifies that a Integer field is of type "Integer".
descriptor = self.get_descriptor({})
editable_fields = descriptor.editable_metadata_fields
......@@ -171,3 +172,194 @@ class EditableMetadataFieldsTest(unittest.TestCase):
self.assertEqual(explicitly_set, test_field['explicitly_set'])
self.assertEqual(inheritable, test_field['inheritable'])
class TestSerialize(unittest.TestCase):
""" Tests the serialize, method, which is not dependent on type. """
def test_serialize(self):
assert_equals('null', serialize_field(None))
assert_equals('-2', serialize_field(-2))
assert_equals('"2"', serialize_field('2'))
assert_equals('-3.41', serialize_field(-3.41))
assert_equals('"2.589"', serialize_field('2.589'))
assert_equals('false', serialize_field(False))
assert_equals('"false"', serialize_field('false'))
assert_equals('"fAlse"', serialize_field('fAlse'))
assert_equals('"hat box"', serialize_field('hat box'))
assert_equals('{"bar": "hat", "frog": "green"}', serialize_field({'bar': 'hat', 'frog' : 'green'}))
assert_equals('[3.5, 5.6]', serialize_field([3.5, 5.6]))
assert_equals('["foo", "bar"]', serialize_field(['foo', 'bar']))
assert_equals('"2012-12-31T23:59:59Z"', serialize_field("2012-12-31T23:59:59Z"))
assert_equals('"1 day 12 hours 59 minutes 59 seconds"',
serialize_field("1 day 12 hours 59 minutes 59 seconds"))
class TestDeserialize(unittest.TestCase):
def assertDeserializeEqual(self, expected, arg):
"""
Asserts the result of deserialize_field.
"""
assert_equals(expected, deserialize_field(self.test_field(), arg))
def assertDeserializeNonString(self):
"""
Asserts input value is returned for None or something that is not a string.
For all types, 'null' is also always returned as None.
"""
self.assertDeserializeEqual(None, None)
self.assertDeserializeEqual(3.14, 3.14)
self.assertDeserializeEqual(True, True)
self.assertDeserializeEqual([10], [10])
self.assertDeserializeEqual({}, {})
self.assertDeserializeEqual([], [])
self.assertDeserializeEqual(None, 'null')
class TestDeserializeInteger(TestDeserialize):
""" Tests deserialize as related to Integer type. """
test_field = Integer
def test_deserialize(self):
self.assertDeserializeEqual(-2, '-2')
self.assertDeserializeEqual("450", '"450"')
# False can be parsed as a int (converts to 0)
self.assertDeserializeEqual(False, 'false')
# True can be parsed as a int (converts to 1)
self.assertDeserializeEqual(True, 'true')
# 2.78 can be converted to int, so the string will be deserialized
self.assertDeserializeEqual(-2.78, '-2.78')
def test_deserialize_unsupported_types(self):
self.assertDeserializeEqual('[3]', '[3]')
# '2.78' cannot be converted to int, so input value is returned
self.assertDeserializeEqual('"-2.78"', '"-2.78"')
# 'false' cannot be converted to int, so input value is returned
self.assertDeserializeEqual('"false"', '"false"')
self.assertDeserializeNonString()
class TestDeserializeFloat(TestDeserialize):
""" Tests deserialize as related to Float type. """
test_field = Float
def test_deserialize(self):
self.assertDeserializeEqual( -2, '-2')
self.assertDeserializeEqual("450", '"450"')
self.assertDeserializeEqual(-2.78, '-2.78')
self.assertDeserializeEqual("0.45", '"0.45"')
# False can be parsed as a float (converts to 0)
self.assertDeserializeEqual(False, 'false')
# True can be parsed as a float (converts to 1)
self.assertDeserializeEqual( True, 'true')
def test_deserialize_unsupported_types(self):
self.assertDeserializeEqual('[3]', '[3]')
# 'false' cannot be converted to float, so input value is returned
self.assertDeserializeEqual('"false"', '"false"')
self.assertDeserializeNonString()
class TestDeserializeBoolean(TestDeserialize):
""" Tests deserialize as related to Boolean type. """
test_field = Boolean
def test_deserialize(self):
# json.loads converts the value to Python bool
self.assertDeserializeEqual(False, 'false')
self.assertDeserializeEqual(True, 'true')
# json.loads fails, string value is returned.
self.assertDeserializeEqual('False', 'False')
self.assertDeserializeEqual('True', 'True')
# json.loads deserializes as a string
self.assertDeserializeEqual('false', '"false"')
self.assertDeserializeEqual('fAlse', '"fAlse"')
self.assertDeserializeEqual("TruE", '"TruE"')
# 2.78 can be converted to a bool, so the string will be deserialized
self.assertDeserializeEqual(-2.78, '-2.78')
self.assertDeserializeNonString()
class TestDeserializeString(TestDeserialize):
""" Tests deserialize as related to String type. """
test_field = String
def test_deserialize(self):
self.assertDeserializeEqual('hAlf', '"hAlf"')
self.assertDeserializeEqual('false', '"false"')
self.assertDeserializeEqual('single quote', 'single quote')
def test_deserialize_unsupported_types(self):
self.assertDeserializeEqual('3.4', '3.4')
self.assertDeserializeEqual('false', 'false')
self.assertDeserializeEqual('2', '2')
self.assertDeserializeEqual('[3]', '[3]')
self.assertDeserializeNonString()
class TestDeserializeAny(TestDeserialize):
""" Tests deserialize as related to Any type. """
test_field = Any
def test_deserialize(self):
self.assertDeserializeEqual('hAlf', '"hAlf"')
self.assertDeserializeEqual('false', '"false"')
self.assertDeserializeEqual({'bar': 'hat', 'frog' : 'green'}, '{"bar": "hat", "frog": "green"}')
self.assertDeserializeEqual([3.5, 5.6], '[3.5, 5.6]')
self.assertDeserializeEqual('[', '[')
self.assertDeserializeEqual(False, 'false')
self.assertDeserializeEqual(3.4, '3.4')
self.assertDeserializeNonString()
class TestDeserializeList(TestDeserialize):
""" Tests deserialize as related to List type. """
test_field = List
def test_deserialize(self):
self.assertDeserializeEqual(['foo', 'bar'], '["foo", "bar"]')
self.assertDeserializeEqual([3.5, 5.6], '[3.5, 5.6]')
self.assertDeserializeEqual([], '[]')
def test_deserialize_unsupported_types(self):
self.assertDeserializeEqual('3.4', '3.4')
self.assertDeserializeEqual('false', 'false')
self.assertDeserializeEqual('2', '2')
self.assertDeserializeNonString()
class TestDeserializeDate(TestDeserialize):
""" Tests deserialize as related to Date type. """
test_field = Date
def test_deserialize(self):
self.assertDeserializeEqual('2012-12-31T23:59:59Z', "2012-12-31T23:59:59Z")
self.assertDeserializeEqual('2012-12-31T23:59:59Z', '"2012-12-31T23:59:59Z"')
self.assertDeserializeNonString()
class TestDeserializeTimedelta(TestDeserialize):
""" Tests deserialize as related to Timedelta type. """
test_field = Timedelta
def test_deserialize(self):
self.assertDeserializeEqual('1 day 12 hours 59 minutes 59 seconds',
'1 day 12 hours 59 minutes 59 seconds')
self.assertDeserializeEqual('1 day 12 hours 59 minutes 59 seconds',
'"1 day 12 hours 59 minutes 59 seconds"')
self.assertDeserializeNonString()
......@@ -123,9 +123,6 @@ class TimeLimitDescriptor(TimeLimitFields, XMLEditingDescriptor, XmlDescriptor):
module_class = TimeLimitModule
# For remembering when a student started, and when they should end
stores_state = True
@classmethod
def definition_from_xml(cls, xml_object, system):
children = []
......
......@@ -86,7 +86,6 @@ class VideoDescriptor(VideoFields,
MetadataOnlyEditingDescriptor,
RawDescriptor):
module_class = VideoModule
stores_state = True
template_dir_name = "video"
@property
......
......@@ -5,6 +5,7 @@ from lxml import etree
from pkg_resources import resource_string, resource_listdir
from django.http import Http404
from django.conf import settings
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
......@@ -147,11 +148,11 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
'caption_asset_path': caption_asset_path,
'show_captions': self.show_captions,
'start': self.start_time,
'end': self.end_time
'end': self.end_time,
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
})
class VideoAlphaDescriptor(VideoAlphaFields, RawDescriptor):
module_class = VideoAlphaModule
stores_state = True
template_dir_name = "videoalpha"
......@@ -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, Dict, 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,
......@@ -64,11 +63,11 @@ class WordCloudFields(object):
scope=Scope.user_state,
default=[]
)
all_words = Object(
all_words = Dict(
help="All possible words from all students.",
scope=Scope.content
)
top_words = Object(
top_words = Dict(
help="Top num_top_words words for word cloud.",
scope=Scope.content
)
......@@ -239,4 +238,3 @@ class WordCloudDescriptor(MetadataOnlyEditingDescriptor, RawDescriptor, WordClou
"""Descriptor for WordCloud Xmodule."""
module_class = WordCloudModule
template_dir_name = 'word_cloud'
stores_state = True
......@@ -327,10 +327,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# Attributes for inspection of the descriptor
# Indicates whether the xmodule state should be
# stored in a database (independent of shared state)
stores_state = False
# This indicates whether the xmodule is a problem-type.
# It should respond to max_score() and grade(). It can be graded or ungraded
# (like a practice problem).
......
......@@ -6,7 +6,7 @@ import sys
from collections import namedtuple
from lxml import etree
from xblock.core import Object, Scope
from xblock.core import Dict, Scope
from xmodule.x_module import (XModuleDescriptor, policy_key)
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
......@@ -79,12 +79,53 @@ class AttrMap(_AttrMapBase):
return _AttrMapBase.__new__(_cls, from_xml, to_xml)
def serialize_field(value):
"""
Return a string version of the value (where value is the JSON-formatted, internally stored value).
By default, this is the result of calling json.dumps on the input value.
"""
return json.dumps(value)
def deserialize_field(field, value):
"""
Deserialize the string version to the value stored internally.
Note that this is not the same as the value returned by from_json, as model types typically store
their value internally as JSON. By default, this method will return the result of calling json.loads
on the supplied value, unless json.loads throws a TypeError, or the type of the value returned by json.loads
is not supported for this class (from_json throws an Error). In either of those cases, this method returns
the input value.
"""
try:
deserialized = json.loads(value)
if deserialized is None:
return deserialized
try:
field.from_json(deserialized)
return deserialized
except (ValueError, TypeError):
# Support older serialized version, which was just a string, not result of json.dumps.
# If the deserialized version cannot be converted to the type (via from_json),
# just return the original value. For example, if a string value of '3.4' was
# stored for a String field (before we started storing the result of json.dumps),
# then it would be deserialized as 3.4, but 3.4 is not supported for a String
# field. Therefore field.from_json(3.4) will throw an Error, and we should
# actually return the original value of '3.4'.
return value
except (ValueError, TypeError):
# Support older serialized version.
return value
class XmlDescriptor(XModuleDescriptor):
"""
Mixin class for standardized parsing of from xml
"""
xml_attributes = Object(help="Map of unhandled xml attributes, used only for storage between import and export",
xml_attributes = Dict(help="Map of unhandled xml attributes, used only for storage between import and export",
default={}, scope=Scope.settings)
# Extension to append to filename paths
......@@ -120,25 +161,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: deserialize_field(field, val)
to_xml = lambda val : serialize_field(val)
return AttrMap(from_xml, to_xml)
return AttrMap()
@classmethod
def definition_from_xml(cls, xml_object, system):
......@@ -188,7 +219,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 +276,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 +288,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 +377,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 +402,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
......
<course filename="6.002_Spring_2012" slug="6.002_Spring_2012" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="6.002 Spring 2012" start="2015-07-17T12:00" course="full" org="edX" show_timezone="true"/>
<course filename="6.002_Spring_2012" slug="6.002_Spring_2012" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="6.002 Spring 2012" start="2015-07-17T12:00" course="full" org="edX" show_timezone="true" advanced_modules="[&quot;videoalpha&quot;]"/>
Feature: Video Alpha component
As a student, I want to view course videos in LMS.
Scenario: Autoplay is enabled in LMS
Given the course has a Video component
Then when I view the video it has autoplay enabled
#pylint: disable=C0111
#pylint: disable=W0613
#pylint: disable=W0621
from lettuce import world, step
from lettuce.django import django_url
from common import TEST_COURSE_NAME, TEST_SECTION_NAME, i_am_registered_for_the_course, section_location
############### ACTIONS ####################
@step('when I view the video it has autoplay enabled')
def does_autoplay(step):
assert(world.css_find('.videoalpha')[0]['data-autoplay'] == 'True')
@step('the course has a Video component')
def view_videoalpha(step):
coursename = TEST_COURSE_NAME.replace(' ', '_')
i_am_registered_for_the_course(step, coursename)
# Make sure we have a videoalpha
add_videoalpha_to_course(coursename)
chapter_name = TEST_SECTION_NAME.replace(" ", "_")
section_name = chapter_name
url = django_url('/courses/edx/Test_Course/Test_Course/courseware/%s/%s' %
(chapter_name, section_name))
world.browser.visit(url)
def add_videoalpha_to_course(course):
template_name = 'i4x://edx/templates/videoalpha/default'
world.ItemFactory.create(parent_location=section_location(course),
template=template_name,
display_name='Video Alpha 1')
......@@ -364,7 +364,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca
else:
return (None, None)
if not (problem_descriptor.stores_state and problem_descriptor.has_score):
if not problem_descriptor.has_score:
# These are not problems, and do not have a score
return (None, None)
......
......@@ -26,7 +26,6 @@ def mock_field(scope, name):
def mock_descriptor(fields=[], lms_fields=[]):
descriptor = Mock()
descriptor.stores_state = True
descriptor.location = location('def_id')
descriptor.module_class.fields = fields
descriptor.module_class.lms.fields = lms_fields
......
......@@ -13,6 +13,7 @@ from xmodule.modulestore.django import modulestore
import courseware.views as views
from xmodule.modulestore import Location
from pytz import UTC
class Stub():
......@@ -63,7 +64,7 @@ class ViewsTestCase(TestCase):
def setUp(self):
self.user = User.objects.create(username='dummy', password='123456',
email='test@mit.edu')
self.date = datetime.datetime(2013, 1, 22)
self.date = datetime.datetime(2013, 1, 22, tzinfo=UTC)
self.course_id = 'edX/toy/2012_Fall'
self.enrollment = CourseEnrollment.objects.get_or_create(user=self.user,
course_id=self.course_id,
......
......@@ -172,8 +172,9 @@ for name, value in ENV_TOKENS.get("CODE_JAIL", {}).items():
COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", [])
############### Mixed Related(Secure/Not-Secure) Items ##########
# If segment.io key specified, load it and turn on segment IO if the feature flag is set
SEGMENT_IO_LMS_KEY = ENV_TOKENS.get('SEGMENT_IO_LMS_KEY')
SEGMENT_IO_LMS_KEY = AUTH_TOKENS.get('SEGMENT_IO_LMS_KEY')
if SEGMENT_IO_LMS_KEY:
MITX_FEATURES['SEGMENT_IO_LMS'] = ENV_TOKENS.get('SEGMENT_IO_LMS', False)
......
......@@ -18,6 +18,7 @@
data-start="${start}"
data-end="${end}"
data-caption-asset-path="${caption_asset_path}"
data-autoplay="${autoplay}"
>
<div class="tc-wrapper">
<article class="video-wrapper">
......
% if settings.MITX_FEATURES.get('SEGMENT_IO_LMS'):
<!-- begin Segment.io -->
<script type="text/javascript">
// Leaving this line out of the feature flag block is intentional. Pulling the line outside of the if statement allows it to serve as its own dummy object.
var analytics=analytics||[];analytics.load=function(e){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=("https:"===document.location.protocol?"https://":"http://")+"d2dq2ahtl5zl1z.cloudfront.net/analytics.js/v1/"+e+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n);var r=function(e){return function(){analytics.push([e].concat(Array.prototype.slice.call(arguments,0)))}},i=["identify","track","trackLink","trackForm","trackClick","trackSubmit","pageview","ab","alias","ready"];for(var s=0;s<i.length;s++)analytics[i[s]]=r(i[s])};
% if settings.MITX_FEATURES.get('SEGMENT_IO_LMS'):
analytics.load("${ settings.SEGMENT_IO_LMS_KEY }");
% if user.is_authenticated():
......@@ -11,14 +13,6 @@
});
% endif
% endif
</script>
<!-- end Segment.io -->
% else:
<!-- dummy segment.io -->
<script type="text/javascript">
var analytics = {
track: function() { return; }
};
</script>
<!-- end dummy segment.io -->
% endif
......@@ -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
......
"""
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
......
......@@ -52,7 +52,7 @@ def sass_cmd(watch=false, debug=false)
"sass #{debug ? '--debug-info' : '--style compressed'} " +
"--load-path #{sass_load_paths.join(' ')} " +
"--require ./common/static/sass/bourbon/lib/bourbon.rb " +
"#{watch ? '--watch' : '--update'} #{sass_watch_paths.join(' ')}"
"#{watch ? '--watch' : '--update'} -E utf-8 #{sass_watch_paths.join(' ')}"
end
desc "Compile all assets"
......@@ -78,7 +78,7 @@ namespace :assets do
end
{:xmodule => [:install_python_prereqs],
:coffee => [:install_node_prereqs],
:coffee => [:install_node_prereqs, :'assets:coffee:clobber'],
:sass => [:install_ruby_prereqs, :preprocess]}.each_pair do |asset_type, prereq_tasks|
desc "Compile all #{asset_type} assets"
task asset_type => prereq_tasks do
......@@ -127,6 +127,11 @@ namespace :assets do
multitask :coffee => 'assets:xmodule'
namespace :coffee do
multitask :debug => 'assets:xmodule:debug'
desc "Remove compiled coffeescript files"
task :clobber do
FileList['*/static/coffee/**/*.js'].each {|f| File.delete(f)}
end
end
namespace :xmodule do
......
......@@ -61,10 +61,10 @@ def template_jasmine_runner(lib)
yield File.expand_path(template_output)
end
def jasmine_browser(url, wait=10)
def jasmine_browser(url, jitter=3, wait=10)
# Jitter starting the browser so that the tests don't all try and
# start the browser simultaneously
sleep(rand(3))
sleep(rand(jitter))
sh("python -m webbrowser -t '#{url}'")
sleep(wait)
end
......@@ -87,6 +87,15 @@ end
end
end
desc "Open jasmine tests for #{system} in your default browser, and dynamically recompile coffeescript"
task :'browser:watch' => :'assets:coffee:_watch' do
django_for_jasmine(system, true) do |jasmine_url|
jasmine_browser(jasmine_url, jitter=0, wait=0)
end
puts "Press ENTER to terminate".red
$stdin.gets
end
desc "Use phantomjs to run jasmine tests for #{system} from the console"
task :phantomjs do
Rake::Task[:assets].invoke(system, 'jasmine')
......
......@@ -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@4d8735e883#egg=XBlock
-e git+https://github.com/edx/codejail.git@0a1b468#egg=codejail
-e git+https://github.com/edx/diff-cover.git@v0.1.1#egg=diff_cover
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment