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> ...@@ -75,3 +75,4 @@ Frances Botsford <frances@edx.org>
Jonah Stanley <Jonah_Stanley@brown.edu> Jonah Stanley <Jonah_Stanley@brown.edu>
Slater Victoroff <slater.r.victoroff@gmail.com> Slater Victoroff <slater.r.victoroff@gmail.com>
Peter Fogg <peter.p.fogg@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: ...@@ -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, If you are running these commands using the [`zsh`](http://www.zsh.org/) shell,
zsh will assume that you are doing 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`, 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 and fail. To fix this, just surround the argument with quotation marks, so that
you're running `rake "django-admin[syncdb]"`. you're running `rake "django-admin[syncdb]"`.
......
...@@ -28,11 +28,18 @@ Feature: Advanced (manual) course policy ...@@ -28,11 +28,18 @@ Feature: Advanced (manual) course policy
Scenario: Test how multi-line input appears Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio 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 Then it is displayed as formatted
And I reload the page And I reload the page
Then it is displayed as formatted 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 Scenario: Test automatic quoting of non-JSON values
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I create a non-JSON value not in quotes When I create a non-JSON value not in quotes
......
...@@ -2,8 +2,7 @@ ...@@ -2,8 +2,7 @@
#pylint: disable=W0621 #pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from nose.tools import assert_false, assert_equal, assert_regexp_matches
from nose.tools import assert_false, assert_equal
""" """
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html 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): ...@@ -52,9 +51,9 @@ def edit_the_value_of_a_policy_key_and_save(step):
change_display_name_value(step, '"foo"') change_display_name_value(step, '"foo"')
@step('I create a JSON object as a value$') @step('I create a JSON object as a value for "(.*)"$')
def create_JSON_object(step): def create_JSON_object(step, key):
change_display_name_value(step, '{"key": "value", "key_2": "value_2"}') change_value(step, key, '{"key": "value", "key_2": "value_2"}')
@step('I create a non-JSON value not in quotes$') @step('I create a non-JSON value not in quotes$')
...@@ -82,7 +81,12 @@ def they_are_alphabetized(step): ...@@ -82,7 +81,12 @@ def they_are_alphabetized(step):
@step('it is displayed as formatted$') @step('it is displayed as formatted$')
def it_is_formatted(step): 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') @step('it is displayed as a string')
...@@ -124,11 +128,16 @@ def get_display_name_value(): ...@@ -124,11 +128,16 @@ def get_display_name_value():
def change_display_name_value(step, new_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") g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
display_name = get_display_name_value() current_value = world.css_find(VALUE_CSS)[index].value
for count in range(len(display_name)): g._element.send_keys(Keys.CONTROL + Keys.END)
for count in range(len(current_value)):
g._element.send_keys(Keys.END, Keys.BACK_SPACE) g._element.send_keys(Keys.END, Keys.BACK_SPACE)
# Must delete "" before typing the JSON value # Must delete "" before typing the JSON value
g._element.send_keys(Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_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): ...@@ -41,7 +41,9 @@ def i_see_five_settings_with_values(step):
@step('I can modify the display name') @step('I can modify the display name')
def i_can_modify_the_display_name(step): 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() verify_modified_display_name()
...@@ -172,7 +174,7 @@ def verify_modified_randomization(): ...@@ -172,7 +174,7 @@ def verify_modified_randomization():
def verify_modified_display_name(): 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(): def verify_modified_display_name_with_special_chars():
......
...@@ -19,6 +19,24 @@ class ChecklistTestCase(CourseTestCase): ...@@ -19,6 +19,24 @@ class ChecklistTestCase(CourseTestCase):
modulestore = get_modulestore(self.course.location) modulestore = get_modulestore(self.course.location)
return modulestore.get_item(self.course.location).checklists 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): def test_get_checklists(self):
""" Tests the get checklists method. """ """ Tests the get checklists method. """
checklists_url = get_url_reverse('Checklists', self.course) checklists_url = get_url_reverse('Checklists', self.course)
...@@ -31,9 +49,9 @@ class ChecklistTestCase(CourseTestCase): ...@@ -31,9 +49,9 @@ class ChecklistTestCase(CourseTestCase):
self.course.checklists = None self.course.checklists = None
modulestore = get_modulestore(self.course.location) modulestore = get_modulestore(self.course.location)
modulestore.update_metadata(self.course.location, own_metadata(self.course)) 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) response = self.client.get(checklists_url)
self.assertEquals(payload, response.content) self.assertEqual(payload, response.content)
def test_update_checklists_no_index(self): def test_update_checklists_no_index(self):
""" No checklist index, should return all of them. """ """ No checklist index, should return all of them. """
...@@ -43,7 +61,8 @@ class ChecklistTestCase(CourseTestCase): ...@@ -43,7 +61,8 @@ class ChecklistTestCase(CourseTestCase):
'name': self.course.location.name}) 'name': self.course.location.name})
returned_checklists = json.loads(self.client.get(update_url).content) 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): def test_update_checklists_index_ignored_on_get(self):
""" Checklist index ignored on get. """ """ Checklist index ignored on get. """
...@@ -53,7 +72,8 @@ class ChecklistTestCase(CourseTestCase): ...@@ -53,7 +72,8 @@ class ChecklistTestCase(CourseTestCase):
'checklist_index': 1}) 'checklist_index': 1})
returned_checklists = json.loads(self.client.get(update_url).content) 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): def test_update_checklists_post_no_index(self):
""" No checklist index, will error on post. """ """ No checklist index, will error on post. """
...@@ -78,13 +98,18 @@ class ChecklistTestCase(CourseTestCase): ...@@ -78,13 +98,18 @@ class ChecklistTestCase(CourseTestCase):
'course': self.course.location.course, 'course': self.course.location.course,
'name': self.course.location.name, 'name': self.course.location.name,
'checklist_index': 2}) 'checklist_index': 2})
def get_first_item(checklist):
return checklist['items'][0]
payload = self.course.checklists[2] payload = self.course.checklists[2]
self.assertFalse(payload.get('is_checked')) self.assertFalse(get_first_item(payload).get('is_checked'))
payload['is_checked'] = True get_first_item(payload)['is_checked'] = True
returned_checklist = json.loads(self.client.post(update_url, json.dumps(payload), "application/json").content) returned_checklist = json.loads(self.client.post(update_url, json.dumps(payload), "application/json").content)
self.assertTrue(returned_checklist.get('is_checked')) self.assertTrue(get_first_item(returned_checklist).get('is_checked'))
self.assertEqual(self.get_persisted_checklists()[2], returned_checklist) pers = self.get_persisted_checklists()
self.compare_checklists(pers[2], returned_checklist)
def test_update_checklists_delete_unsupported(self): def test_update_checklists_delete_unsupported(self):
""" Delete operation is not supported. """ """ Delete operation is not supported. """
...@@ -93,4 +118,4 @@ class ChecklistTestCase(CourseTestCase): ...@@ -93,4 +118,4 @@ class ChecklistTestCase(CourseTestCase):
'name': self.course.location.name, 'name': self.course.location.name,
'checklist_index': 100}) 'checklist_index': 100})
response = self.client.delete(update_url) response = self.client.delete(update_url)
self.assertContains(response, 'Unsupported request', status_code=400) self.assertContains(response, 'Unsupported request', status_code=400)
\ No newline at end of file
...@@ -402,8 +402,11 @@ def course_advanced_updates(request, org, course, name): ...@@ -402,8 +402,11 @@ def course_advanced_updates(request, org, course, name):
request_body.update({'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 filter_tabs = False
try:
response_json = json.dumps(CourseMetadata.update_from_json(location, response_json = json.dumps(CourseMetadata.update_from_json(location,
request_body, request_body,
filter_tabs=filter_tabs)) 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") return HttpResponse(response_json, mimetype="application/json")
...@@ -5,7 +5,6 @@ Namespace defining common fields used by Studio for all blocks ...@@ -5,7 +5,6 @@ Namespace defining common fields used by Studio for all blocks
import datetime import datetime
from xblock.core import Namespace, Scope, ModelType, String from xblock.core import Namespace, Scope, ModelType, String
from xmodule.fields import StringyBoolean
class DateTuple(ModelType): class DateTuple(ModelType):
...@@ -28,4 +27,3 @@ class CmsNamespace(Namespace): ...@@ -28,4 +27,3 @@ class CmsNamespace(Namespace):
""" """
published_date = DateTuple(help="Date when the module was published", scope=Scope.settings) 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) 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 ...@@ -5,6 +5,7 @@ from django.contrib.auth.models import Group
from datetime import datetime from datetime import datetime
from factory import DjangoModelFactory, SubFactory, PostGenerationMethodCall, post_generation, Sequence from factory import DjangoModelFactory, SubFactory, PostGenerationMethodCall, post_generation, Sequence
from uuid import uuid4 from uuid import uuid4
from pytz import UTC
# Factories don't have __init__ methods, and are self documenting # Factories don't have __init__ methods, and are self documenting
# pylint: disable=W0232 # pylint: disable=W0232
...@@ -46,8 +47,8 @@ class UserFactory(DjangoModelFactory): ...@@ -46,8 +47,8 @@ class UserFactory(DjangoModelFactory):
is_staff = False is_staff = False
is_active = True is_active = True
is_superuser = False is_superuser = False
last_login = datetime(2012, 1, 1) last_login = datetime(2012, 1, 1, tzinfo=UTC)
date_joined = datetime(2011, 1, 1) date_joined = datetime(2011, 1, 1, tzinfo=UTC)
@post_generation @post_generation
def profile(obj, create, extracted, **kwargs): 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 copy
import logging
import math import math
import operator import operator
import re import re
import numpy import numpy
import numbers
import scipy.constants 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 (Word, nums, Literal,
from pyparsing import ZeroOrMore, OneOrMore, StringStart ZeroOrMore, MatchFirst,
from pyparsing import StringEnd, Optional, Forward Optional, Forward,
from pyparsing import CaselessLiteral, Group, StringEnd CaselessLiteral,
from pyparsing import NoMatch, stringEnd, alphanums stringEnd, Suppress, Combine)
default_functions = {'sin': numpy.sin, DEFAULT_FUNCTIONS = {'sin': numpy.sin,
'cos': numpy.cos, 'cos': numpy.cos,
'tan': numpy.tan, 'tan': numpy.tan,
'sec': calcfunctions.sec,
'csc': calcfunctions.csc,
'cot': calcfunctions.cot,
'sqrt': numpy.sqrt, 'sqrt': numpy.sqrt,
'log10': numpy.log10, 'log10': numpy.log10,
'log2': numpy.log2, 'log2': numpy.log2,
'ln': numpy.log, 'ln': numpy.log,
'exp': numpy.exp,
'arccos': numpy.arccos, 'arccos': numpy.arccos,
'arcsin': numpy.arcsin, 'arcsin': numpy.arcsin,
'arctan': numpy.arctan, 'arctan': numpy.arctan,
'arcsec': calcfunctions.arcsec,
'arccsc': calcfunctions.arccsc,
'arccot': calcfunctions.arccot,
'abs': numpy.abs, 'abs': numpy.abs,
'fact': math.factorial, '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, 'e': numpy.e,
'pi': numpy.pi, 'pi': numpy.pi,
'k': scipy.constants.k, 'k': scipy.constants.k,
...@@ -37,65 +66,166 @@ default_variables = {'j': numpy.complex(0, 1), ...@@ -37,65 +66,166 @@ default_variables = {'j': numpy.complex(0, 1),
'q': scipy.constants.e '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): class UndefinedVariable(Exception):
def raiseself(self): """
''' Helper so we can use inside of a lambda ''' Used to indicate the student input of a variable, which was unused by the
raise self instructor.
"""
pass
general_whitespace = re.compile('[^\w]+')
def check_variables(string, variables): 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 Otherwise, raise an UndefinedVariable containing all bad variables.
elegant approach pretty hopeless.
achar = reduce(lambda a,b:a|b ,map(Literal,alphas)) # Any alphabetic character Pyparsing uses a left-to-right parser, which makes a more
undefined_variable = achar + Word(alphanums) elegant approach pretty hopeless.
undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself()) """
varnames = varnames | undefined_variable general_whitespace = re.compile('[^\\w]+')
''' # List of all alnums in string
possible_variables = re.split(general_whitespace, string) # List of all alnums in string possible_variables = re.split(general_whitespace, string)
bad_variables = list() bad_variables = []
for v in possible_variables: for var in possible_variables:
if len(v) == 0: if len(var) == 0:
continue 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 continue
if v not in variables: if var not in variables:
bad_variables.append(v) bad_variables.append(var)
if len(bad_variables) > 0: if len(bad_variables) > 0:
raise UndefinedVariable(' '.join(bad_variables)) 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): def evaluator(variables, functions, string, cs=False):
''' """
Evaluate an expression. Variables are passed as a dictionary Evaluate an expression. Variables are passed as a dictionary
from string to value. Unary functions are passed as a dictionary from string to value. Unary functions are passed as a dictionary
from string to function. Variables must be floats. from string to function. Variables must be floats.
cs: Case sensitive 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_variables.update(variables)
all_functions.update(functions) all_functions.update(functions)
...@@ -113,122 +243,59 @@ def evaluator(variables, functions, string, cs=False): ...@@ -113,122 +243,59 @@ def evaluator(variables, functions, string, cs=False):
if string.strip() == "": if string.strip() == "":
return float('nan') 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 # SI suffixes and percent
number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch()) number_suffix = MatchFirst([Literal(k) for k in SUFFIXES.keys()])
(dot, minus, plus, times, div, lpar, rpar, exp) = map(Literal, ".-+*/()^") plus_minus = Literal('+') | Literal('-')
times_div = Literal('*') | Literal('/')
number_part = Word(nums) number_part = Word(nums)
# 0.33 or 7 or .34 or 16. # 0.33 or 7 or .34 or 16.
inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part) 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 # 0.33k or -17
number = (Optional(minus | plus) + inner_number number = (inner_number
+ Optional(CaselessLiteral("E") + Optional((plus | minus)) + number_part) + Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part)
+ Optional(number_suffix)) + Optional(number_suffix))
number = number.setParseAction(number_parse_action) # Convert to number number.setParseAction(number_parse_action) # Convert to number
# Predefine recursive variables # Predefine recursive variables
expr = Forward() expr = Forward()
factor = Forward()
# Handle variables passed in.
def sreduce(f, l): # E.g. if we have {'R':0.5}, we make the substitution.
''' Same as reduce, but handle len 1 and len 0 lists sensibly ''' # We sort the list so that var names (like "e2") match before
if len(l) == 0: # mathematical constants (like "e"). This is kind of a hack.
return NoMatch() all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True)
if len(l) == 1: varnames = MatchFirst([CasedLiteral(k) for k in all_variables_keys])
return l[0] varnames.setParseAction(
return reduce(f, l) lambda x: [all_variables[k] for k in x]
)
# 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 all_variables were empty, then pyparsing wants
if len(all_variables) > 0: # varnames = NoMatch()
# We sort the list so that var names (like "e2") match before # this is not the case, as all_variables contains the defaults
# 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()
# Same thing for functions. # Same thing for functions.
if len(all_functions) > 0: all_functions_keys = sorted(all_functions.keys(), key=len, reverse=True)
funcnames = sreduce(lambda x, y: x | y, funcnames = MatchFirst([CasedLiteral(k) for k in all_functions_keys])
map(lambda x: CasedLiteral(x), all_functions.keys())) function = funcnames + Suppress("(") + expr + Suppress(")")
function = funcnames + lpar.suppress() + expr + rpar.suppress() function.setParseAction(
function.setParseAction(func_parse_action) lambda x: [all_functions[x[0]](x[1])]
else: )
function = NoMatch()
atom = number | function | varnames | Suppress("(") + expr + Suppress(")")
atom = number | function | varnames | lpar + expr + rpar
factor << (atom + ZeroOrMore(exp + atom)).setParseAction(exp_parse_action) # 7^6 # Do the following in the correct order to preserve order of operation
paritem = factor + ZeroOrMore(Literal('||') + factor) # 5k || 4k pow_term = atom + ZeroOrMore(Suppress("^") + atom)
paritem = paritem.setParseAction(parallel) pow_term.setParseAction(exp_parse_action) # 7^6
term = paritem + ZeroOrMore((times | div) + paritem) # 7 * 5 / 4 - 3 par_term = pow_term + ZeroOrMore(Suppress('||') + pow_term) # 5k || 4k
term = term.setParseAction(prod_parse_action) par_term.setParseAction(parallel)
expr << Optional((plus | minus)) + term + ZeroOrMore((plus | minus) + term) # -5 + 4 - 3 prod_term = par_term + ZeroOrMore(times_div + par_term) # 7 * 5 / 4 - 3
expr = expr.setParseAction(sum_parse_action) 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] 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): ...@@ -194,6 +194,105 @@ class EvaluatorTest(unittest.TestCase):
arctan_angles = arcsin_angles arctan_angles = arcsin_angles
self.assert_function_values('arctan', arctan_inputs, arctan_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): def test_other_functions(self):
""" """
Test the non-trig functions provided in calc.py Test the non-trig functions provided in calc.py
......
...@@ -1738,6 +1738,7 @@ class FormulaResponse(LoncapaResponse): ...@@ -1738,6 +1738,7 @@ class FormulaResponse(LoncapaResponse):
student_variables = dict() student_variables = dict()
# ranges give numerical ranges for testing # ranges give numerical ranges for testing
for var in ranges: for var in ranges:
# TODO: allow specified ranges (i.e. integers and complex numbers) for random variables
value = random.uniform(*ranges[var]) value = random.uniform(*ranges[var])
instructor_variables[str(var)] = value instructor_variables[str(var)] = value
student_variables[str(var)] = value student_variables[str(var)] = value
......
...@@ -6,7 +6,7 @@ from xmodule.x_module import XModule ...@@ -6,7 +6,7 @@ from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xmodule.exceptions import InvalidDefinitionError from xmodule.exceptions import InvalidDefinitionError
from xblock.core import String, Scope, Object from xblock.core import String, Scope, Dict
DEFAULT = "_DEFAULT_GROUP" DEFAULT = "_DEFAULT_GROUP"
...@@ -32,9 +32,9 @@ def group_from_value(groups, v): ...@@ -32,9 +32,9 @@ def group_from_value(groups, v):
class ABTestFields(object): class ABTestFields(object):
group_portions = Object(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content) group_portions = Dict(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_assignments = Dict(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_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) experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content)
has_children = True has_children = True
......
...@@ -125,6 +125,5 @@ class AnnotatableModule(AnnotatableFields, XModule): ...@@ -125,6 +125,5 @@ class AnnotatableModule(AnnotatableFields, XModule):
class AnnotatableDescriptor(AnnotatableFields, RawDescriptor): class AnnotatableDescriptor(AnnotatableFields, RawDescriptor):
module_class = AnnotatableModule module_class = AnnotatableModule
stores_state = True
template_dir_name = "annotatable" template_dir_name = "annotatable"
mako_template = "widgets/raw-edit.html" mako_template = "widgets/raw-edit.html"
...@@ -18,8 +18,8 @@ from .progress import Progress ...@@ -18,8 +18,8 @@ from .progress import Progress
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.exceptions import NotFoundError, ProcessingError
from xblock.core import Scope, String, Boolean, Object from xblock.core import Scope, String, Boolean, Dict, Integer, Float
from .fields import Timedelta, Date, StringyInteger, StringyFloat from .fields import Timedelta, Date
from django.utils.timezone import UTC from django.utils.timezone import UTC
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -65,8 +65,8 @@ class ComplexEncoder(json.JSONEncoder): ...@@ -65,8 +65,8 @@ class ComplexEncoder(json.JSONEncoder):
class CapaFields(object): class CapaFields(object):
attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state) attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state)
max_attempts = StringyInteger( max_attempts = Integer(
display_name="Maximum Attempts", 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.", 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 values={"min": 0}, scope=Scope.settings
...@@ -95,12 +95,12 @@ class CapaFields(object): ...@@ -95,12 +95,12 @@ class CapaFields(object):
{"display_name": "Per Student", "value": "per_student"}] {"display_name": "Per Student", "value": "per_student"}]
) )
data = String(help="XML data for the problem", scope=Scope.content) 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={}) correct_map = Dict(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) input_state = Dict(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) 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) 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) seed = Integer(help="Random seed for this student", scope=Scope.user_state)
weight = StringyFloat( weight = Float(
display_name="Problem Weight", 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.", 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}, values={"min": 0, "step": .1},
...@@ -315,7 +315,7 @@ class CapaModule(CapaFields, XModule): ...@@ -315,7 +315,7 @@ class CapaModule(CapaFields, XModule):
# If the user has forced the save button to display, # If the user has forced the save button to display,
# then show it as long as the problem is not closed # then show it as long as the problem is not closed
# (past due / too many attempts) # (past due / too many attempts)
if self.force_save_button == "true": if self.force_save_button:
return not self.closed() return not self.closed()
else: else:
is_survey_question = (self.max_attempts == 0) is_survey_question = (self.max_attempts == 0)
...@@ -902,7 +902,6 @@ class CapaDescriptor(CapaFields, RawDescriptor): ...@@ -902,7 +902,6 @@ class CapaDescriptor(CapaFields, RawDescriptor):
module_class = CapaModule module_class = CapaModule
stores_state = True
has_score = True has_score = True
template_dir_name = 'problem' template_dir_name = 'problem'
mako_template = "widgets/problem-edit.html" mako_template = "widgets/problem-edit.html"
......
...@@ -5,10 +5,10 @@ from pkg_resources import resource_string ...@@ -5,10 +5,10 @@ from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from .x_module import XModule 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 xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple from collections import namedtuple
from .fields import Date, StringyFloat, StringyInteger, StringyBoolean from .fields import Date
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -53,27 +53,27 @@ class CombinedOpenEndedFields(object): ...@@ -53,27 +53,27 @@ class CombinedOpenEndedFields(object):
help="This name appears in the horizontal navigation at the top of the page.", help="This name appears in the horizontal navigation at the top of the page.",
default="Open Ended Grading", scope=Scope.settings 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) 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", state = String(help="Which step within the current task that the student is on.", default="initial",
scope=Scope.user_state) 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) 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, help="If the problem is ready to be reset or not.", default=False,
scope=Scope.user_state scope=Scope.user_state
) )
attempts = StringyInteger( attempts = Integer(
display_name="Maximum Attempts", display_name="Maximum Attempts",
help="The number of times the student can try to answer this problem.", default=1, help="The number of times the student can try to answer this problem.", default=1,
scope=Scope.settings, values = {"min" : 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) is_graded = Boolean(display_name="Graded", help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
accept_file_upload = StringyBoolean( accept_file_upload = Boolean(
display_name="Allow File Uploads", display_name="Allow File Uploads",
help="Whether or not the student can submit files as a response.", default=False, scope=Scope.settings 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", 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.", 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 default=False, scope=Scope.settings
...@@ -86,7 +86,7 @@ class CombinedOpenEndedFields(object): ...@@ -86,7 +86,7 @@ class CombinedOpenEndedFields(object):
) )
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings) version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content) data = String(help="XML data for the problem", scope=Scope.content)
weight = StringyFloat( weight = Float(
display_name="Problem Weight", 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.", 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"} scope=Scope.settings, values = {"min" : 0 , "step": ".1"}
...@@ -239,7 +239,6 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor): ...@@ -239,7 +239,6 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
mako_template = "widgets/open-ended-edit.html" mako_template = "widgets/open-ended-edit.html"
module_class = CombinedOpenEndedModule module_class = CombinedOpenEndedModule
stores_state = True
has_score = True has_score = True
always_recalculate_grades = True always_recalculate_grades = True
template_dir_name = "combinedopenended" template_dir_name = "combinedopenended"
......
...@@ -92,7 +92,7 @@ class ConditionalModule(ConditionalFields, XModule): ...@@ -92,7 +92,7 @@ class ConditionalModule(ConditionalFields, XModule):
if xml_value and self.required_modules: if xml_value and self.required_modules:
for module in self.required_modules: for module in self.required_modules:
if not hasattr(module, attr_name): 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 # the descriptor of a required module to have a property but
# for the resulting module to be a (flavor of) ErrorModule. # for the resulting module to be a (flavor of) ErrorModule.
# So just log and return false. # So just log and return false.
...@@ -161,7 +161,6 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor): ...@@ -161,7 +161,6 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor):
filename_extension = "xml" filename_extension = "xml"
stores_state = True
has_score = False has_score = False
@staticmethod @staticmethod
......
...@@ -15,7 +15,7 @@ from xmodule.util.decorators import lazyproperty ...@@ -15,7 +15,7 @@ from xmodule.util.decorators import lazyproperty
from xmodule.graders import grader_from_conf from xmodule.graders import grader_from_conf
import json 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 .fields import Date
from django.utils.timezone import UTC from django.utils.timezone import UTC
from xmodule.util import date_utils from xmodule.util import date_utils
...@@ -154,25 +154,25 @@ class CourseFields(object): ...@@ -154,25 +154,25 @@ class CourseFields(object):
start = Date(help="Start time when this module is visible", scope=Scope.settings) start = Date(help="Start time when this module is visible", scope=Scope.settings)
end = Date(help="Date that this class ends", 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) 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) 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) 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) 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) 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_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", help="Map of topics names to ids",
scope=Scope.settings 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) 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) 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) 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) 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) 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) 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 = Boolean(scope=Scope.settings, default=True)
allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False) allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False)
advanced_modules = List(help="Beta modules used in your course", scope=Scope.settings) advanced_modules = List(help="Beta modules used in your course", scope=Scope.settings)
......
...@@ -6,7 +6,6 @@ from xblock.core import ModelType ...@@ -6,7 +6,6 @@ from xblock.core import ModelType
import datetime import datetime
import dateutil.parser import dateutil.parser
from xblock.core import Integer, Float, Boolean
from django.utils.timezone import UTC from django.utils.timezone import UTC
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -93,42 +92,3 @@ class Timedelta(ModelType): ...@@ -93,42 +92,3 @@ class Timedelta(ModelType):
if cur_value > 0: if cur_value > 0:
values.append("%d %s" % (cur_value, attr)) values.append("%d %s" % (cur_value, attr))
return ' '.join(values) 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): ...@@ -183,7 +183,6 @@ class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
module_class = FolditModule module_class = FolditModule
filename_extension = "xml" filename_extension = "xml"
stores_state = True
has_score = True has_score = True
template_dir_name = "foldit" template_dir_name = "foldit"
......
...@@ -140,12 +140,16 @@ class @VideoCaptionAlpha extends SubviewAlpha ...@@ -140,12 +140,16 @@ class @VideoCaptionAlpha extends SubviewAlpha
hideCaptions: (hide_captions) => hideCaptions: (hide_captions) =>
if hide_captions if hide_captions
type = 'hide_transcript'
@$('.hide-subtitles').attr('title', 'Turn on captions') @$('.hide-subtitles').attr('title', 'Turn on captions')
@el.addClass('closed') @el.addClass('closed')
else else
type = 'show_transcript'
@$('.hide-subtitles').attr('title', 'Turn off captions') @$('.hide-subtitles').attr('title', 'Turn off captions')
@el.removeClass('closed') @el.removeClass('closed')
@scrollCaption() @scrollCaption()
@video.log type,
currentTime: @player.currentTime
$.cookie('hide_captions', hide_captions, expires: 3650, path: '/') $.cookie('hide_captions', hide_captions, expires: 3650, path: '/')
captionHeight: -> captionHeight: ->
......
...@@ -45,6 +45,8 @@ class @VideoPlayerAlpha extends SubviewAlpha ...@@ -45,6 +45,8 @@ class @VideoPlayerAlpha extends SubviewAlpha
if @video.show_captions is true if @video.show_captions is true
@caption = new VideoCaptionAlpha @caption = new VideoCaptionAlpha
el: @el el: @el
video: @video
player: @
youtubeId: @video.youtubeId('1.0') youtubeId: @video.youtubeId('1.0')
currentSpeed: @currentSpeed() currentSpeed: @currentSpeed()
captionAssetPath: @video.caption_asset_path captionAssetPath: @video.caption_asset_path
...@@ -66,7 +68,16 @@ class @VideoPlayerAlpha extends SubviewAlpha ...@@ -66,7 +68,16 @@ class @VideoPlayerAlpha extends SubviewAlpha
if @video.end if @video.end
# work in AS3, not HMLT5. but iframe use AS3 # work in AS3, not HMLT5. but iframe use AS3
@playerVars.end = @video.end @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' if @video.videoType is 'html5'
@video.playerType = 'browser'
@player = new HTML5Video.Player @video.el, @player = new HTML5Video.Player @video.el,
playerVars: @playerVars, playerVars: @playerVars,
videoSources: @video.html5Sources, videoSources: @video.html5Sources,
...@@ -79,6 +90,7 @@ class @VideoPlayerAlpha extends SubviewAlpha ...@@ -79,6 +90,7 @@ class @VideoPlayerAlpha extends SubviewAlpha
youTubeId = @video.videos['1.0'] youTubeId = @video.videos['1.0']
else else
youTubeId = @video.youtubeId() youTubeId = @video.youtubeId()
@video.playerType = 'youtube'
@player = new YT.Player @video.id, @player = new YT.Player @video.id,
playerVars: @playerVars playerVars: @playerVars
videoId: youTubeId videoId: youTubeId
...@@ -99,7 +111,7 @@ class @VideoPlayerAlpha extends SubviewAlpha ...@@ -99,7 +111,7 @@ class @VideoPlayerAlpha extends SubviewAlpha
@video.log 'load_video' @video.log 'load_video'
if @video.videoType is 'html5' if @video.videoType is 'html5'
@player.setPlaybackRate @video.speed @player.setPlaybackRate @video.speed
unless onTouchBasedDevice() if not onTouchBasedDevice() and $('.video:first').data('autoplay') is 'True'
$('.video-load-complete:first').data('video').player.play() $('.video-load-complete:first').data('video').player.play()
onStateChange: (event) => onStateChange: (event) =>
...@@ -235,13 +247,18 @@ class @VideoPlayerAlpha extends SubviewAlpha ...@@ -235,13 +247,18 @@ class @VideoPlayerAlpha extends SubviewAlpha
if @video.videoType is 'youtube' if @video.videoType is 'youtube'
if @video.show_captions is true if @video.show_captions is true
@caption.currentSpeed = newSpeed @caption.currentSpeed = newSpeed
if @video.videoType is 'html5'
@player.setPlaybackRate newSpeed # We request the reloading of the video in the case when YouTube is in Flash player mode,
else if @video.videoType is 'youtube' # 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() if @isPlaying()
@player.loadVideoById(@video.youtubeId(), @currentTime) @player.loadVideoById(@video.youtubeId(), @currentTime)
else else
@player.cueVideoById(@video.youtubeId(), @currentTime) @player.cueVideoById(@video.youtubeId(), @currentTime)
else if @video.videoType is 'html5'
@player.setPlaybackRate newSpeed
if @video.videoType is 'youtube' if @video.videoType is 'youtube'
@updatePlayTime @currentTime @updatePlayTime @currentTime
...@@ -262,11 +279,15 @@ class @VideoPlayerAlpha extends SubviewAlpha ...@@ -262,11 +279,15 @@ class @VideoPlayerAlpha extends SubviewAlpha
toggleFullScreen: (event) => toggleFullScreen: (event) =>
event.preventDefault() event.preventDefault()
if @el.hasClass('fullscreen') if @el.hasClass('fullscreen')
type = 'not_fullscreen'
@$('.add-fullscreen').attr('title', 'Fill browser') @$('.add-fullscreen').attr('title', 'Fill browser')
@el.removeClass('fullscreen') @el.removeClass('fullscreen')
else else
type = 'fullscreen'
@el.addClass('fullscreen') @el.addClass('fullscreen')
@$('.add-fullscreen').attr('title', 'Exit fill browser') @$('.add-fullscreen').attr('title', 'Exit fill browser')
@video.log type,
currentTime: @currentTime
if @video.show_captions is true if @video.show_captions is true
@caption.resize() @caption.resize()
......
...@@ -823,7 +823,6 @@ class CombinedOpenEndedV1Descriptor(): ...@@ -823,7 +823,6 @@ class CombinedOpenEndedV1Descriptor():
module_class = CombinedOpenEndedV1Module module_class = CombinedOpenEndedV1Module
filename_extension = "xml" filename_extension = "xml"
stores_state = True
has_score = True has_score = True
template_dir_name = "combinedopenended" template_dir_name = "combinedopenended"
......
...@@ -731,7 +731,6 @@ class OpenEndedDescriptor(): ...@@ -731,7 +731,6 @@ class OpenEndedDescriptor():
module_class = OpenEndedModule module_class = OpenEndedModule
filename_extension = "xml" filename_extension = "xml"
stores_state = True
has_score = True has_score = True
template_dir_name = "openended" template_dir_name = "openended"
......
...@@ -286,7 +286,6 @@ class SelfAssessmentDescriptor(): ...@@ -286,7 +286,6 @@ class SelfAssessmentDescriptor():
module_class = SelfAssessmentModule module_class = SelfAssessmentModule
filename_extension = "xml" filename_extension = "xml"
stores_state = True
has_score = True has_score = True
template_dir_name = "selfassessment" template_dir_name = "selfassessment"
......
...@@ -10,8 +10,8 @@ from .x_module import XModule ...@@ -10,8 +10,8 @@ from .x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from .timeinfo import TimeInfo from .timeinfo import TimeInfo
from xblock.core import Object, String, Scope from xblock.core import Dict, String, Scope, Boolean, Integer, Float
from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean from xmodule.fields import Date
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
from open_ended_grading_classes import combined_open_ended_rubric from open_ended_grading_classes import combined_open_ended_rubric
...@@ -21,7 +21,6 @@ log = logging.getLogger(__name__) ...@@ -21,7 +21,6 @@ log = logging.getLogger(__name__)
USE_FOR_SINGLE_LOCATION = False USE_FOR_SINGLE_LOCATION = False
LINK_TO_LOCATION = "" LINK_TO_LOCATION = ""
TRUE_DICT = [True, "True", "true", "TRUE"]
MAX_SCORE = 1 MAX_SCORE = 1
IS_GRADED = False IS_GRADED = False
...@@ -29,7 +28,7 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please ...@@ -29,7 +28,7 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please
class PeerGradingFields(object): class PeerGradingFields(object):
use_for_single_location = StringyBoolean( use_for_single_location = Boolean(
display_name="Show Single Problem", display_name="Show Single Problem",
help='When True, only the single problem specified by "Link to Problem Location" is shown. ' 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.', 'When False, a panel is displayed with all problems available for peer grading.',
...@@ -40,22 +39,22 @@ class PeerGradingFields(object): ...@@ -40,22 +39,22 @@ class PeerGradingFields(object):
help='The location of the problem being graded. Only used when "Show Single Problem" is True.', help='The location of the problem being graded. Only used when "Show Single Problem" is True.',
default=LINK_TO_LOCATION, scope=Scope.settings default=LINK_TO_LOCATION, scope=Scope.settings
) )
is_graded = StringyBoolean( is_graded = Boolean(
display_name="Graded", display_name="Graded",
help='Defines whether the student gets credit for grading this problem. Only used when "Show Single Problem" is True.', 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 default=IS_GRADED, scope=Scope.settings
) )
due_date = Date(help="Due date that should be displayed.", default=None, 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) 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, help="The maximum grade that a student can receive for this problem.", default=MAX_SCORE,
scope=Scope.settings, values={"min": 0} 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.", help="Student data for a given peer grading problem.",
scope=Scope.user_state scope=Scope.user_state
) )
weight = StringyFloat( weight = Float(
display_name="Problem Weight", 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.", 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"} scope=Scope.settings, values={"min": 0, "step": ".1"}
...@@ -85,7 +84,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -85,7 +84,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
else: else:
self.peer_gs = MockPeerGradingService() self.peer_gs = MockPeerGradingService()
if self.use_for_single_location in TRUE_DICT: if self.use_for_single_location:
try: try:
self.linked_problem = modulestore().get_instance(self.system.course_id, self.link_to_location) self.linked_problem = modulestore().get_instance(self.system.course_id, self.link_to_location)
except: except:
...@@ -113,7 +112,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -113,7 +112,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
if not self.ajax_url.endswith("/"): if not self.ajax_url.endswith("/"):
self.ajax_url = self.ajax_url + "/" 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): if not isinstance(self.max_grade, int):
raise TypeError("max_grade needs to be an integer.") raise TypeError("max_grade needs to be an integer.")
...@@ -147,7 +146,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -147,7 +146,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
""" """
if self.closed(): if self.closed():
return self.peer_grading_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() return self.peer_grading()
else: else:
return self.peer_grading_problem({'location': self.link_to_location})['html'] return self.peer_grading_problem({'location': self.link_to_location})['html']
...@@ -204,7 +203,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -204,7 +203,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
'score': score, 'score': score,
'total': max_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 return score_dict
try: try:
...@@ -239,7 +238,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -239,7 +238,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
randomization, and 5/7 on another randomization, and 5/7 on another
''' '''
max_grade = None 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 max_grade = self.max_grade
return max_grade return max_grade
...@@ -557,7 +556,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -557,7 +556,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
Show individual problem interface Show individual problem interface
''' '''
if get is None or get.get('location') is None: 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 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 a dev_facing_error
log.error( log.error(
...@@ -603,7 +602,6 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor): ...@@ -603,7 +602,6 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
module_class = PeerGradingModule module_class = PeerGradingModule
filename_extension = "xml" filename_extension = "xml"
stores_state = True
has_score = True has_score = True
always_recalculate_grades = True always_recalculate_grades = True
template_dir_name = "peer_grading" template_dir_name = "peer_grading"
......
...@@ -19,7 +19,7 @@ from xmodule.x_module import XModule ...@@ -19,7 +19,7 @@ from xmodule.x_module import XModule
from xmodule.stringify import stringify_children from xmodule.stringify import stringify_children
from xmodule.mako_module import MakoModuleDescriptor from xmodule.mako_module import MakoModuleDescriptor
from xmodule.xml_module import XmlDescriptor 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__) log = logging.getLogger(__name__)
...@@ -30,7 +30,7 @@ class PollFields(object): ...@@ -30,7 +30,7 @@ class PollFields(object):
voted = Boolean(help="Whether this student has voted on the poll", scope=Scope.user_state, default=False) 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_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=[]) answers = List(help="Poll answers from xml", scope=Scope.content, default=[])
question = String(help="Poll question", scope=Scope.content, default='') question = String(help="Poll question", scope=Scope.content, default='')
...@@ -141,7 +141,6 @@ class PollDescriptor(PollFields, MakoModuleDescriptor, XmlDescriptor): ...@@ -141,7 +141,6 @@ class PollDescriptor(PollFields, MakoModuleDescriptor, XmlDescriptor):
module_class = PollModule module_class = PollModule
template_dir_name = 'poll' template_dir_name = 'poll'
stores_state = True
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
......
...@@ -94,7 +94,6 @@ class RandomizeDescriptor(RandomizeFields, SequenceDescriptor): ...@@ -94,7 +94,6 @@ class RandomizeDescriptor(RandomizeFields, SequenceDescriptor):
filename_extension = "xml" filename_extension = "xml"
stores_state = True
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
......
...@@ -121,8 +121,6 @@ class SequenceDescriptor(SequenceFields, MakoModuleDescriptor, XmlDescriptor): ...@@ -121,8 +121,6 @@ class SequenceDescriptor(SequenceFields, MakoModuleDescriptor, XmlDescriptor):
mako_template = 'widgets/sequence-edit.html' mako_template = 'widgets/sequence-edit.html'
module_class = SequenceModule 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 = {'coffee': [resource_string(__name__, 'js/src/sequence/edit.coffee')]}
js_module_name = "SequenceDescriptor" js_module_name = "SequenceDescriptor"
......
"""Tests for classes defined in fields.py.""" """Tests for classes defined in fields.py."""
import datetime import datetime
import unittest import unittest
from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean
from django.utils.timezone import UTC from django.utils.timezone import UTC
from xmodule.fields import Date, Timedelta
class DateTest(unittest.TestCase): class DateTest(unittest.TestCase):
date = Date() date = Date()
...@@ -70,54 +71,22 @@ class DateTest(unittest.TestCase): ...@@ -70,54 +71,22 @@ class DateTest(unittest.TestCase):
"2012-12-31T23:00:01-01:00") "2012-12-31T23:00:01-01:00")
class StringyIntegerTest(unittest.TestCase): class TimedeltaTest(unittest.TestCase):
def assertEquals(self, expected, arg): delta = Timedelta()
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 test_from_json(self):
self.assertEqual(
def assertEquals(self, expected, arg): TimedeltaTest.delta.from_json('1 day 12 hours 59 minutes 59 seconds'),
self.assertEqual(expected, StringyBoolean().from_json(arg)) datetime.timedelta(days=1, hours=12, minutes=59, seconds=59)
)
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.assertEqual(
self.assertEquals(123, 123) 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 @@ ...@@ -2,11 +2,12 @@
#pylint: disable=C0111 #pylint: disable=C0111
from xmodule.x_module import XModuleFields from xmodule.x_module import XModuleFields
from xblock.core import Scope, String, Object, Boolean from xblock.core import Scope, String, Dict, Boolean, Integer, Float, Any, List
from xmodule.fields import Date, StringyInteger, StringyFloat from xmodule.fields import Date, Timedelta
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field
import unittest import unittest
from .import test_system from .import test_system
from nose.tools import assert_equals
from mock import Mock from mock import Mock
...@@ -17,11 +18,11 @@ class CrazyJsonString(String): ...@@ -17,11 +18,11 @@ class CrazyJsonString(String):
class TestFields(object): class TestFields(object):
# Will be returned by editable_metadata_fields. # 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. # Will not be returned by editable_metadata_fields because filtered out by non_editable_metadata_fields.
due = Date(scope=Scope.settings) due = Date(scope=Scope.settings)
# Will not be returned by editable_metadata_fields because is not 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. # 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', display_name = String(scope=Scope.settings, default='local default', display_name='Local Display Name',
help='local help') help='local help')
...@@ -33,9 +34,9 @@ class TestFields(object): ...@@ -33,9 +34,9 @@ class TestFields(object):
{'display_name': 'second', 'value': 'value b'}] {'display_name': 'second', 'value': 'value b'}]
) )
# Used for testing select type # 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 # 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 # Used for testing that Booleans get mapped to select type
boolean_select = Boolean(scope=Scope.settings) boolean_select = Boolean(scope=Scope.settings)
...@@ -104,7 +105,7 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -104,7 +105,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
def test_type_and_options(self): def test_type_and_options(self):
# test_display_name_field verifies that a String field is of type "Generic". # 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({}) descriptor = self.get_descriptor({})
editable_fields = descriptor.editable_metadata_fields editable_fields = descriptor.editable_metadata_fields
...@@ -171,3 +172,194 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -171,3 +172,194 @@ class EditableMetadataFieldsTest(unittest.TestCase):
self.assertEqual(explicitly_set, test_field['explicitly_set']) self.assertEqual(explicitly_set, test_field['explicitly_set'])
self.assertEqual(inheritable, test_field['inheritable']) 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): ...@@ -123,9 +123,6 @@ class TimeLimitDescriptor(TimeLimitFields, XMLEditingDescriptor, XmlDescriptor):
module_class = TimeLimitModule module_class = TimeLimitModule
# For remembering when a student started, and when they should end
stores_state = True
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
children = [] children = []
......
...@@ -86,7 +86,6 @@ class VideoDescriptor(VideoFields, ...@@ -86,7 +86,6 @@ class VideoDescriptor(VideoFields,
MetadataOnlyEditingDescriptor, MetadataOnlyEditingDescriptor,
RawDescriptor): RawDescriptor):
module_class = VideoModule module_class = VideoModule
stores_state = True
template_dir_name = "video" template_dir_name = "video"
@property @property
......
...@@ -5,6 +5,7 @@ from lxml import etree ...@@ -5,6 +5,7 @@ from lxml import etree
from pkg_resources import resource_string, resource_listdir from pkg_resources import resource_string, resource_listdir
from django.http import Http404 from django.http import Http404
from django.conf import settings
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
...@@ -147,11 +148,11 @@ class VideoAlphaModule(VideoAlphaFields, XModule): ...@@ -147,11 +148,11 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
'caption_asset_path': caption_asset_path, 'caption_asset_path': caption_asset_path,
'show_captions': self.show_captions, 'show_captions': self.show_captions,
'start': self.start_time, 'start': self.start_time,
'end': self.end_time 'end': self.end_time,
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
}) })
class VideoAlphaDescriptor(VideoAlphaFields, RawDescriptor): class VideoAlphaDescriptor(VideoAlphaFields, RawDescriptor):
module_class = VideoAlphaModule module_class = VideoAlphaModule
stores_state = True
template_dir_name = "videoalpha" template_dir_name = "videoalpha"
...@@ -14,8 +14,7 @@ from xmodule.raw_module import RawDescriptor ...@@ -14,8 +14,7 @@ from xmodule.raw_module import RawDescriptor
from xmodule.editing_module import MetadataOnlyEditingDescriptor from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xblock.core import Scope, Object, Boolean, List from xblock.core import Scope, Dict, Boolean, List, Integer
from fields import StringyBoolean, StringyInteger
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -32,21 +31,21 @@ def pretty_bool(value): ...@@ -32,21 +31,21 @@ def pretty_bool(value):
class WordCloudFields(object): class WordCloudFields(object):
"""XFields for word cloud.""" """XFields for word cloud."""
num_inputs = StringyInteger( num_inputs = Integer(
display_name="Inputs", display_name="Inputs",
help="Number of text boxes available for students to input words/sentences.", help="Number of text boxes available for students to input words/sentences.",
scope=Scope.settings, scope=Scope.settings,
default=5, default=5,
values={"min": 1} values={"min": 1}
) )
num_top_words = StringyInteger( num_top_words = Integer(
display_name="Maximum Words", display_name="Maximum Words",
help="Maximum number of words to be displayed in generated word cloud.", help="Maximum number of words to be displayed in generated word cloud.",
scope=Scope.settings, scope=Scope.settings,
default=250, default=250,
values={"min": 1} values={"min": 1}
) )
display_student_percents = StringyBoolean( display_student_percents = Boolean(
display_name="Show Percents", display_name="Show Percents",
help="Statistics are shown for entered words near that word.", help="Statistics are shown for entered words near that word.",
scope=Scope.settings, scope=Scope.settings,
...@@ -64,11 +63,11 @@ class WordCloudFields(object): ...@@ -64,11 +63,11 @@ class WordCloudFields(object):
scope=Scope.user_state, scope=Scope.user_state,
default=[] default=[]
) )
all_words = Object( all_words = Dict(
help="All possible words from all students.", help="All possible words from all students.",
scope=Scope.content scope=Scope.content
) )
top_words = Object( top_words = Dict(
help="Top num_top_words words for word cloud.", help="Top num_top_words words for word cloud.",
scope=Scope.content scope=Scope.content
) )
...@@ -239,4 +238,3 @@ class WordCloudDescriptor(MetadataOnlyEditingDescriptor, RawDescriptor, WordClou ...@@ -239,4 +238,3 @@ class WordCloudDescriptor(MetadataOnlyEditingDescriptor, RawDescriptor, WordClou
"""Descriptor for WordCloud Xmodule.""" """Descriptor for WordCloud Xmodule."""
module_class = WordCloudModule module_class = WordCloudModule
template_dir_name = 'word_cloud' template_dir_name = 'word_cloud'
stores_state = True
...@@ -327,10 +327,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -327,10 +327,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# Attributes for inspection of the descriptor # 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. # This indicates whether the xmodule is a problem-type.
# It should respond to max_score() and grade(). It can be graded or ungraded # It should respond to max_score() and grade(). It can be graded or ungraded
# (like a practice problem). # (like a practice problem).
......
...@@ -6,7 +6,7 @@ import sys ...@@ -6,7 +6,7 @@ import sys
from collections import namedtuple from collections import namedtuple
from lxml import etree 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.x_module import (XModuleDescriptor, policy_key)
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
...@@ -79,12 +79,53 @@ class AttrMap(_AttrMapBase): ...@@ -79,12 +79,53 @@ class AttrMap(_AttrMapBase):
return _AttrMapBase.__new__(_cls, from_xml, to_xml) 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): class XmlDescriptor(XModuleDescriptor):
""" """
Mixin class for standardized parsing of from xml 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) default={}, scope=Scope.settings)
# Extension to append to filename paths # Extension to append to filename paths
...@@ -120,25 +161,15 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -120,25 +161,15 @@ class XmlDescriptor(XModuleDescriptor):
metadata_to_export_to_policy = ('discussion_topics') metadata_to_export_to_policy = ('discussion_topics')
# A dictionary mapping xml attribute names AttrMaps that describe how @classmethod
# to import and export them def get_map_for_field(cls, attr):
# Allow json to specify either the string "true", or the bool True. The string is preferred. for field in set(cls.fields + cls.lms.fields):
to_bool = lambda val: val == 'true' or val == True if field.name == attr:
from_bool = lambda val: str(val).lower() from_xml = lambda val: deserialize_field(field, val)
bool_map = AttrMap(to_bool, from_bool) to_xml = lambda val : serialize_field(val)
return AttrMap(from_xml, to_xml)
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,
}
return AttrMap()
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
...@@ -188,7 +219,6 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -188,7 +219,6 @@ class XmlDescriptor(XModuleDescriptor):
filepath, location.url(), str(err)) filepath, location.url(), str(err))
raise Exception, msg, sys.exc_info()[2] raise Exception, msg, sys.exc_info()[2]
@classmethod @classmethod
def load_definition(cls, xml_object, system, location): def load_definition(cls, xml_object, system, location):
'''Load a descriptor definition from the specified xml_object. '''Load a descriptor definition from the specified xml_object.
...@@ -246,7 +276,7 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -246,7 +276,7 @@ class XmlDescriptor(XModuleDescriptor):
# don't load these # don't load these
continue 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) metadata[attr] = attr_map.from_xml(val)
return metadata return metadata
...@@ -258,7 +288,7 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -258,7 +288,7 @@ class XmlDescriptor(XModuleDescriptor):
through the attrmap. Updates the metadata dict in place. through the attrmap. Updates the metadata dict in place.
""" """
for attr in policy: 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]) metadata[cls._translate(attr)] = attr_map.from_xml(policy[attr])
@classmethod @classmethod
...@@ -347,7 +377,7 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -347,7 +377,7 @@ class XmlDescriptor(XModuleDescriptor):
def export_to_xml(self, resource_fs): 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 underneath it. May also write required resources out to resource_fs
Assumes that modules have single parentage (that no module appears twice Assumes that modules have single parentage (that no module appears twice
...@@ -372,7 +402,7 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -372,7 +402,7 @@ class XmlDescriptor(XModuleDescriptor):
"""Get the value for this attribute that we want to store. """Get the value for this attribute that we want to store.
(Possible format conversion through an AttrMap). (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]) return attr_map.to_xml(self._model_data[attr])
# Add the non-inherited metadata # 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 ...@@ -364,7 +364,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca
else: else:
return (None, None) 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 # These are not problems, and do not have a score
return (None, None) return (None, None)
......
...@@ -26,7 +26,6 @@ def mock_field(scope, name): ...@@ -26,7 +26,6 @@ def mock_field(scope, name):
def mock_descriptor(fields=[], lms_fields=[]): def mock_descriptor(fields=[], lms_fields=[]):
descriptor = Mock() descriptor = Mock()
descriptor.stores_state = True
descriptor.location = location('def_id') descriptor.location = location('def_id')
descriptor.module_class.fields = fields descriptor.module_class.fields = fields
descriptor.module_class.lms.fields = lms_fields descriptor.module_class.lms.fields = lms_fields
......
...@@ -13,6 +13,7 @@ from xmodule.modulestore.django import modulestore ...@@ -13,6 +13,7 @@ from xmodule.modulestore.django import modulestore
import courseware.views as views import courseware.views as views
from xmodule.modulestore import Location from xmodule.modulestore import Location
from pytz import UTC
class Stub(): class Stub():
...@@ -63,7 +64,7 @@ class ViewsTestCase(TestCase): ...@@ -63,7 +64,7 @@ class ViewsTestCase(TestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create(username='dummy', password='123456', self.user = User.objects.create(username='dummy', password='123456',
email='test@mit.edu') 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.course_id = 'edX/toy/2012_Fall'
self.enrollment = CourseEnrollment.objects.get_or_create(user=self.user, self.enrollment = CourseEnrollment.objects.get_or_create(user=self.user,
course_id=self.course_id, course_id=self.course_id,
......
...@@ -172,8 +172,9 @@ for name, value in ENV_TOKENS.get("CODE_JAIL", {}).items(): ...@@ -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", []) 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 # 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: if SEGMENT_IO_LMS_KEY:
MITX_FEATURES['SEGMENT_IO_LMS'] = ENV_TOKENS.get('SEGMENT_IO_LMS', False) MITX_FEATURES['SEGMENT_IO_LMS'] = ENV_TOKENS.get('SEGMENT_IO_LMS', False)
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
data-start="${start}" data-start="${start}"
data-end="${end}" data-end="${end}"
data-caption-asset-path="${caption_asset_path}" data-caption-asset-path="${caption_asset_path}"
data-autoplay="${autoplay}"
> >
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
......
% if settings.MITX_FEATURES.get('SEGMENT_IO_LMS'):
<!-- begin Segment.io --> <!-- begin Segment.io -->
<script type="text/javascript"> <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])}; 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 }"); analytics.load("${ settings.SEGMENT_IO_LMS_KEY }");
% if user.is_authenticated(): % if user.is_authenticated():
...@@ -11,14 +13,6 @@ ...@@ -11,14 +13,6 @@
}); });
% endif % endif
% endif
</script> </script>
<!-- end Segment.io --> <!-- 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"]: ...@@ -98,6 +98,8 @@ if not settings.MITX_FEATURES["USE_CUSTOM_THEME"]:
url(r'^press$', 'student.views.press', name="press"), url(r'^press$', 'student.views.press', name="press"),
url(r'^media-kit$', 'static_template_view.views.render', url(r'^media-kit$', 'static_template_view.views.render',
{'template': 'media-kit.html'}, name="media-kit"), {'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', url(r'^help$', 'static_template_view.views.render',
{'template': 'help.html'}, name="help_edx"), {'template': 'help.html'}, name="help_edx"),
...@@ -125,7 +127,7 @@ for key, value in settings.MKTG_URL_LINK_MAP.items(): ...@@ -125,7 +127,7 @@ for key, value in settings.MKTG_URL_LINK_MAP.items():
continue continue
# These urls are enabled separately # These urls are enabled separately
if key == "ROOT" or key == "COURSES": if key == "ROOT" or key == "COURSES" or key == "FAQ":
continue continue
# Make the assumptions that the templates are all in the same dir # Make the assumptions that the templates are all in the same dir
......
""" """
Namespace that defines fields common to all blocks used in the LMS Namespace that defines fields common to all blocks used in the LMS
""" """
from xblock.core import Namespace, Boolean, Scope, String from xblock.core import Namespace, Boolean, Scope, String, Float
from xmodule.fields import Date, Timedelta, StringyFloat, StringyBoolean from xmodule.fields import Date, Timedelta
class LmsNamespace(Namespace): class LmsNamespace(Namespace):
""" """
Namespace that defines fields common to all blocks used in the LMS 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", help="Whether to display this module in the table of contents",
default=False, default=False,
scope=Scope.settings scope=Scope.settings
...@@ -37,7 +37,7 @@ class LmsNamespace(Namespace): ...@@ -37,7 +37,7 @@ class LmsNamespace(Namespace):
) )
showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed") 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) 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", help="Number of days early to show content to beta users",
default=None, default=None,
scope=Scope.settings scope=Scope.settings
......
...@@ -52,7 +52,7 @@ def sass_cmd(watch=false, debug=false) ...@@ -52,7 +52,7 @@ def sass_cmd(watch=false, debug=false)
"sass #{debug ? '--debug-info' : '--style compressed'} " + "sass #{debug ? '--debug-info' : '--style compressed'} " +
"--load-path #{sass_load_paths.join(' ')} " + "--load-path #{sass_load_paths.join(' ')} " +
"--require ./common/static/sass/bourbon/lib/bourbon.rb " + "--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 end
desc "Compile all assets" desc "Compile all assets"
...@@ -78,7 +78,7 @@ namespace :assets do ...@@ -78,7 +78,7 @@ namespace :assets do
end end
{:xmodule => [:install_python_prereqs], {: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| :sass => [:install_ruby_prereqs, :preprocess]}.each_pair do |asset_type, prereq_tasks|
desc "Compile all #{asset_type} assets" desc "Compile all #{asset_type} assets"
task asset_type => prereq_tasks do task asset_type => prereq_tasks do
...@@ -127,6 +127,11 @@ namespace :assets do ...@@ -127,6 +127,11 @@ namespace :assets do
multitask :coffee => 'assets:xmodule' multitask :coffee => 'assets:xmodule'
namespace :coffee do namespace :coffee do
multitask :debug => 'assets:xmodule:debug' multitask :debug => 'assets:xmodule:debug'
desc "Remove compiled coffeescript files"
task :clobber do
FileList['*/static/coffee/**/*.js'].each {|f| File.delete(f)}
end
end end
namespace :xmodule do namespace :xmodule do
......
...@@ -61,10 +61,10 @@ def template_jasmine_runner(lib) ...@@ -61,10 +61,10 @@ def template_jasmine_runner(lib)
yield File.expand_path(template_output) yield File.expand_path(template_output)
end 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 # Jitter starting the browser so that the tests don't all try and
# start the browser simultaneously # start the browser simultaneously
sleep(rand(3)) sleep(rand(jitter))
sh("python -m webbrowser -t '#{url}'") sh("python -m webbrowser -t '#{url}'")
sleep(wait) sleep(wait)
end end
...@@ -87,6 +87,15 @@ end ...@@ -87,6 +87,15 @@ end
end 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" desc "Use phantomjs to run jasmine tests for #{system} from the console"
task :phantomjs do task :phantomjs do
Rake::Task[:assets].invoke(system, 'jasmine') Rake::Task[:assets].invoke(system, 'jasmine')
......
...@@ -8,6 +8,6 @@ ...@@ -8,6 +8,6 @@
-e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk -e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
# Our libraries: # 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/codejail.git@0a1b468#egg=codejail
-e git+https://github.com/edx/diff-cover.git@v0.1.1#egg=diff_cover -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