Merge branch 'master' into peter-fogg/remove-video-xml
This commit is contained in:
1
AUTHORS
1
AUTHORS
@@ -75,3 +75,4 @@ Frances Botsford <frances@edx.org>
|
||||
Jonah Stanley <Jonah_Stanley@brown.edu>
|
||||
Slater Victoroff <slater.r.victoroff@gmail.com>
|
||||
Peter Fogg <peter.p.fogg@gmail.com>
|
||||
Renzo Lucioni <renzolucioni@gmail.com>
|
||||
88
CHANGELOG.rst
Normal file
88
CHANGELOG.rst
Normal file
@@ -0,0 +1,88 @@
|
||||
Change Log
|
||||
----------
|
||||
|
||||
These are notable changes in edx-platform. This is a rolling list of changes,
|
||||
in roughly chronological order, most recent first. Add your entries at or near
|
||||
the top. Include a label indicating the component affected.
|
||||
|
||||
Studio, LMS: Make ModelTypes more strict about their expected content (for
|
||||
instance, Boolean, Integer, String), but also allow them to hold either the
|
||||
typed value, or a String that can be converted to their typed value. For example,
|
||||
an Integer can contain 3 or '3'. This changed an update to the xblock library.
|
||||
|
||||
LMS: Some errors handling Non-ASCII data in XML courses have been fixed.
|
||||
|
||||
LMS: Add page-load tracking using segment-io (if SEGMENT_IO_LMS_KEY and
|
||||
SEGMENT_IO_LMS feature flag is on)
|
||||
|
||||
Blades: Simplify calc.py (which is used for the Numerical/Formula responses); add trig/other functions.
|
||||
|
||||
LMS: Background colors on login, register, and courseware have been corrected
|
||||
back to white.
|
||||
|
||||
LMS: Accessibility improvements have been made to several courseware and
|
||||
navigation elements.
|
||||
|
||||
LMS: Small design/presentation changes to login and register views.
|
||||
|
||||
LMS: Functionality added to instructor enrollment tab in LMS such that invited
|
||||
student can be auto-enrolled in course or when activating if not current
|
||||
student.
|
||||
|
||||
Blades: Staff debug info is now accessible for Graphical Slider Tool problems.
|
||||
|
||||
Blades: For Video Alpha the events ready, play, pause, seek, and speed change
|
||||
are logged on the server (in the logs).
|
||||
|
||||
Common: Developers can now have private Django settings files.
|
||||
|
||||
Common: Safety code added to prevent anything above the vertical level in the
|
||||
course tree from being marked as version='draft'. It will raise an exception if
|
||||
the code tries to so mark a node. We need the backtraces to figure out where
|
||||
this very infrequent intermittent marking was occurring. It was making courses
|
||||
look different in Studio than in LMS.
|
||||
|
||||
Deploy: MKTG_URLS is now read from env.json.
|
||||
|
||||
Common: Theming makes it possible to change the look of the site, from
|
||||
Stanford.
|
||||
|
||||
Common: Accessibility UI fixes.
|
||||
|
||||
Common: The "duplicate email" error message is more informative.
|
||||
|
||||
Studio: Component metadata settings editor.
|
||||
|
||||
Studio: Autoplay is disabled (only in Studio).
|
||||
|
||||
Studio: Single-click creation for video and discussion components.
|
||||
|
||||
Studio: fixed a bad link in the activation page.
|
||||
|
||||
LMS: Changed the help button text.
|
||||
|
||||
LMS: Fixed failing numeric response (decimal but no trailing digits).
|
||||
|
||||
LMS: XML Error module no longer shows students a stack trace.
|
||||
|
||||
Blades: Videoalpha.
|
||||
|
||||
XModules: Added partial credit for foldit module.
|
||||
|
||||
XModules: Added "randomize" XModule to list of XModule types.
|
||||
|
||||
XModules: Show errors with full descriptors.
|
||||
|
||||
XQueue: Fixed (hopefully) worker crash when the connection to RabbitMQ is
|
||||
dropped suddenly.
|
||||
|
||||
XQueue: Upload file submissions to a specially named bucket in S3.
|
||||
|
||||
Common: Removed request debugger.
|
||||
|
||||
Common: Updated Django to version 1.4.5.
|
||||
|
||||
Common: Updated CodeJail.
|
||||
|
||||
Common: Allow setting of authentication session cookie name.
|
||||
|
||||
@@ -115,7 +115,7 @@ CMS templates. Fortunately, `rake` will do all of this for you! Just run:
|
||||
|
||||
If you are running these commands using the [`zsh`](http://www.zsh.org/) shell,
|
||||
zsh will assume that you are doing
|
||||
[shell globbing](https://en.wikipedia.org/wiki/Glob_(programming)), search for
|
||||
[shell globbing](https://en.wikipedia.org/wiki/Glob_%28programming%29), search for
|
||||
a file in your directory named `django-adminsyncdb` or `django-adminmigrate`,
|
||||
and fail. To fix this, just surround the argument with quotation marks, so that
|
||||
you're running `rake "django-admin[syncdb]"`.
|
||||
|
||||
@@ -28,11 +28,18 @@ Feature: Advanced (manual) course policy
|
||||
|
||||
Scenario: Test how multi-line input appears
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I create a JSON object as a value
|
||||
When I create a JSON object as a value for "discussion_topics"
|
||||
Then it is displayed as formatted
|
||||
And I reload the page
|
||||
Then it is displayed as formatted
|
||||
|
||||
Scenario: Test error if value supplied is of the wrong type
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I create a JSON object as a value for "display_name"
|
||||
Then I get an error on save
|
||||
And I reload the page
|
||||
Then the policy key value is unchanged
|
||||
|
||||
Scenario: Test automatic quoting of non-JSON values
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I create a non-JSON value not in quotes
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
from nose.tools import assert_false, assert_equal
|
||||
from nose.tools import assert_false, assert_equal, assert_regexp_matches
|
||||
|
||||
"""
|
||||
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html
|
||||
@@ -52,9 +51,9 @@ def edit_the_value_of_a_policy_key_and_save(step):
|
||||
change_display_name_value(step, '"foo"')
|
||||
|
||||
|
||||
@step('I create a JSON object as a value$')
|
||||
def create_JSON_object(step):
|
||||
change_display_name_value(step, '{"key": "value", "key_2": "value_2"}')
|
||||
@step('I create a JSON object as a value for "(.*)"$')
|
||||
def create_JSON_object(step, key):
|
||||
change_value(step, key, '{"key": "value", "key_2": "value_2"}')
|
||||
|
||||
|
||||
@step('I create a non-JSON value not in quotes$')
|
||||
@@ -82,7 +81,12 @@ def they_are_alphabetized(step):
|
||||
|
||||
@step('it is displayed as formatted$')
|
||||
def it_is_formatted(step):
|
||||
assert_policy_entries([DISPLAY_NAME_KEY], ['{\n "key": "value",\n "key_2": "value_2"\n}'])
|
||||
assert_policy_entries(['discussion_topics'], ['{\n "key": "value",\n "key_2": "value_2"\n}'])
|
||||
|
||||
|
||||
@step('I get an error on save$')
|
||||
def error_on_save(step):
|
||||
assert_regexp_matches(world.css_text('#notification-error-description'), 'Incorrect setting format')
|
||||
|
||||
|
||||
@step('it is displayed as a string')
|
||||
@@ -124,11 +128,16 @@ def get_display_name_value():
|
||||
|
||||
|
||||
def change_display_name_value(step, new_value):
|
||||
change_value(step, DISPLAY_NAME_KEY, new_value)
|
||||
|
||||
world.css_find(".CodeMirror")[get_index_of(DISPLAY_NAME_KEY)].click()
|
||||
|
||||
def change_value(step, key, new_value):
|
||||
index = get_index_of(key)
|
||||
world.css_find(".CodeMirror")[index].click()
|
||||
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
|
||||
display_name = get_display_name_value()
|
||||
for count in range(len(display_name)):
|
||||
current_value = world.css_find(VALUE_CSS)[index].value
|
||||
g._element.send_keys(Keys.CONTROL + Keys.END)
|
||||
for count in range(len(current_value)):
|
||||
g._element.send_keys(Keys.END, Keys.BACK_SPACE)
|
||||
# Must delete "" before typing the JSON value
|
||||
g._element.send_keys(Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
|
||||
|
||||
@@ -41,7 +41,9 @@ def i_see_five_settings_with_values(step):
|
||||
|
||||
@step('I can modify the display name')
|
||||
def i_can_modify_the_display_name(step):
|
||||
world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill('modified')
|
||||
# Verifying that the display name can be a string containing a floating point value
|
||||
# (to confirm that we don't throw an error because it is of the wrong type).
|
||||
world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill('3.4')
|
||||
verify_modified_display_name()
|
||||
|
||||
|
||||
@@ -172,7 +174,7 @@ def verify_modified_randomization():
|
||||
|
||||
|
||||
def verify_modified_display_name():
|
||||
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, 'modified', True)
|
||||
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, '3.4', True)
|
||||
|
||||
|
||||
def verify_modified_display_name_with_special_chars():
|
||||
|
||||
@@ -19,6 +19,24 @@ class ChecklistTestCase(CourseTestCase):
|
||||
modulestore = get_modulestore(self.course.location)
|
||||
return modulestore.get_item(self.course.location).checklists
|
||||
|
||||
|
||||
def compare_checklists(self, persisted, request):
|
||||
"""
|
||||
Handles url expansion as possible difference and descends into guts
|
||||
:param persisted:
|
||||
:param request:
|
||||
"""
|
||||
self.assertEqual(persisted['short_description'], request['short_description'])
|
||||
compare_urls = (persisted.get('action_urls_expanded') == request.get('action_urls_expanded'))
|
||||
for pers, req in zip(persisted['items'], request['items']):
|
||||
self.assertEqual(pers['short_description'], req['short_description'])
|
||||
self.assertEqual(pers['long_description'], req['long_description'])
|
||||
self.assertEqual(pers['is_checked'], req['is_checked'])
|
||||
if compare_urls:
|
||||
self.assertEqual(pers['action_url'], req['action_url'])
|
||||
self.assertEqual(pers['action_text'], req['action_text'])
|
||||
self.assertEqual(pers['action_external'], req['action_external'])
|
||||
|
||||
def test_get_checklists(self):
|
||||
""" Tests the get checklists method. """
|
||||
checklists_url = get_url_reverse('Checklists', self.course)
|
||||
@@ -31,9 +49,9 @@ class ChecklistTestCase(CourseTestCase):
|
||||
self.course.checklists = None
|
||||
modulestore = get_modulestore(self.course.location)
|
||||
modulestore.update_metadata(self.course.location, own_metadata(self.course))
|
||||
self.assertEquals(self.get_persisted_checklists(), None)
|
||||
self.assertEqual(self.get_persisted_checklists(), None)
|
||||
response = self.client.get(checklists_url)
|
||||
self.assertEquals(payload, response.content)
|
||||
self.assertEqual(payload, response.content)
|
||||
|
||||
def test_update_checklists_no_index(self):
|
||||
""" No checklist index, should return all of them. """
|
||||
@@ -43,7 +61,8 @@ class ChecklistTestCase(CourseTestCase):
|
||||
'name': self.course.location.name})
|
||||
|
||||
returned_checklists = json.loads(self.client.get(update_url).content)
|
||||
self.assertListEqual(self.get_persisted_checklists(), returned_checklists)
|
||||
for pay, resp in zip(self.get_persisted_checklists(), returned_checklists):
|
||||
self.compare_checklists(pay, resp)
|
||||
|
||||
def test_update_checklists_index_ignored_on_get(self):
|
||||
""" Checklist index ignored on get. """
|
||||
@@ -53,7 +72,8 @@ class ChecklistTestCase(CourseTestCase):
|
||||
'checklist_index': 1})
|
||||
|
||||
returned_checklists = json.loads(self.client.get(update_url).content)
|
||||
self.assertListEqual(self.get_persisted_checklists(), returned_checklists)
|
||||
for pay, resp in zip(self.get_persisted_checklists(), returned_checklists):
|
||||
self.compare_checklists(pay, resp)
|
||||
|
||||
def test_update_checklists_post_no_index(self):
|
||||
""" No checklist index, will error on post. """
|
||||
@@ -78,13 +98,18 @@ class ChecklistTestCase(CourseTestCase):
|
||||
'course': self.course.location.course,
|
||||
'name': self.course.location.name,
|
||||
'checklist_index': 2})
|
||||
|
||||
def get_first_item(checklist):
|
||||
return checklist['items'][0]
|
||||
|
||||
payload = self.course.checklists[2]
|
||||
self.assertFalse(payload.get('is_checked'))
|
||||
payload['is_checked'] = True
|
||||
self.assertFalse(get_first_item(payload).get('is_checked'))
|
||||
get_first_item(payload)['is_checked'] = True
|
||||
|
||||
returned_checklist = json.loads(self.client.post(update_url, json.dumps(payload), "application/json").content)
|
||||
self.assertTrue(returned_checklist.get('is_checked'))
|
||||
self.assertEqual(self.get_persisted_checklists()[2], returned_checklist)
|
||||
self.assertTrue(get_first_item(returned_checklist).get('is_checked'))
|
||||
pers = self.get_persisted_checklists()
|
||||
self.compare_checklists(pers[2], returned_checklist)
|
||||
|
||||
def test_update_checklists_delete_unsupported(self):
|
||||
""" Delete operation is not supported. """
|
||||
@@ -93,4 +118,4 @@ class ChecklistTestCase(CourseTestCase):
|
||||
'name': self.course.location.name,
|
||||
'checklist_index': 100})
|
||||
response = self.client.delete(update_url)
|
||||
self.assertContains(response, 'Unsupported request', status_code=400)
|
||||
self.assertContains(response, 'Unsupported request', status_code=400)
|
||||
|
||||
@@ -402,8 +402,11 @@ def course_advanced_updates(request, org, course, name):
|
||||
request_body.update({'tabs': new_tabs})
|
||||
# Indicate that tabs should *not* be filtered out of the metadata
|
||||
filter_tabs = False
|
||||
|
||||
response_json = json.dumps(CourseMetadata.update_from_json(location,
|
||||
try:
|
||||
response_json = json.dumps(CourseMetadata.update_from_json(location,
|
||||
request_body,
|
||||
filter_tabs=filter_tabs))
|
||||
except (TypeError, ValueError), e:
|
||||
return HttpResponseBadRequest("Incorrect setting format. " + str(e), content_type="text/plain")
|
||||
|
||||
return HttpResponse(response_json, mimetype="application/json")
|
||||
|
||||
@@ -5,7 +5,6 @@ Namespace defining common fields used by Studio for all blocks
|
||||
import datetime
|
||||
|
||||
from xblock.core import Namespace, Scope, ModelType, String
|
||||
from xmodule.fields import StringyBoolean
|
||||
|
||||
|
||||
class DateTuple(ModelType):
|
||||
@@ -28,4 +27,3 @@ class CmsNamespace(Namespace):
|
||||
"""
|
||||
published_date = DateTuple(help="Date when the module was published", scope=Scope.settings)
|
||||
published_by = String(help="Id of the user who published this module", scope=Scope.settings)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.contrib.auth.models import Group
|
||||
from datetime import datetime
|
||||
from factory import DjangoModelFactory, SubFactory, PostGenerationMethodCall, post_generation, Sequence
|
||||
from uuid import uuid4
|
||||
from pytz import UTC
|
||||
|
||||
# Factories don't have __init__ methods, and are self documenting
|
||||
# pylint: disable=W0232
|
||||
@@ -46,8 +47,8 @@ class UserFactory(DjangoModelFactory):
|
||||
is_staff = False
|
||||
is_active = True
|
||||
is_superuser = False
|
||||
last_login = datetime(2012, 1, 1)
|
||||
date_joined = datetime(2011, 1, 1)
|
||||
last_login = datetime(2012, 1, 1, tzinfo=UTC)
|
||||
date_joined = datetime(2011, 1, 1, tzinfo=UTC)
|
||||
|
||||
@post_generation
|
||||
def profile(obj, create, extracted, **kwargs):
|
||||
|
||||
@@ -1,34 +1,63 @@
|
||||
"""
|
||||
Parser and evaluator for FormulaResponse and NumericalResponse
|
||||
|
||||
Uses pyparsing to parse. Main function as of now is evaluator().
|
||||
"""
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import math
|
||||
import operator
|
||||
import re
|
||||
|
||||
import numpy
|
||||
import numbers
|
||||
import scipy.constants
|
||||
import calcfunctions
|
||||
|
||||
from pyparsing import Word, alphas, nums, oneOf, Literal
|
||||
from pyparsing import ZeroOrMore, OneOrMore, StringStart
|
||||
from pyparsing import StringEnd, Optional, Forward
|
||||
from pyparsing import CaselessLiteral, Group, StringEnd
|
||||
from pyparsing import NoMatch, stringEnd, alphanums
|
||||
# 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'
|
||||
|
||||
default_functions = {'sin': numpy.sin,
|
||||
from pyparsing import (Word, nums, Literal,
|
||||
ZeroOrMore, MatchFirst,
|
||||
Optional, Forward,
|
||||
CaselessLiteral,
|
||||
stringEnd, Suppress, Combine)
|
||||
|
||||
DEFAULT_FUNCTIONS = {'sin': numpy.sin,
|
||||
'cos': numpy.cos,
|
||||
'tan': numpy.tan,
|
||||
'sec': calcfunctions.sec,
|
||||
'csc': calcfunctions.csc,
|
||||
'cot': calcfunctions.cot,
|
||||
'sqrt': numpy.sqrt,
|
||||
'log10': numpy.log10,
|
||||
'log2': numpy.log2,
|
||||
'ln': numpy.log,
|
||||
'exp': numpy.exp,
|
||||
'arccos': numpy.arccos,
|
||||
'arcsin': numpy.arcsin,
|
||||
'arctan': numpy.arctan,
|
||||
'arcsec': calcfunctions.arcsec,
|
||||
'arccsc': calcfunctions.arccsc,
|
||||
'arccot': calcfunctions.arccot,
|
||||
'abs': numpy.abs,
|
||||
'fact': math.factorial,
|
||||
'factorial': math.factorial
|
||||
'factorial': math.factorial,
|
||||
'sinh': numpy.sinh,
|
||||
'cosh': numpy.cosh,
|
||||
'tanh': numpy.tanh,
|
||||
'sech': calcfunctions.sech,
|
||||
'csch': calcfunctions.csch,
|
||||
'coth': calcfunctions.coth,
|
||||
'arcsinh': numpy.arcsinh,
|
||||
'arccosh': numpy.arccosh,
|
||||
'arctanh': numpy.arctanh,
|
||||
'arcsech': calcfunctions.arcsech,
|
||||
'arccsch': calcfunctions.arccsch,
|
||||
'arccoth': calcfunctions.arccoth
|
||||
}
|
||||
default_variables = {'j': numpy.complex(0, 1),
|
||||
DEFAULT_VARIABLES = {'i': numpy.complex(0, 1),
|
||||
'j': numpy.complex(0, 1),
|
||||
'e': numpy.e,
|
||||
'pi': numpy.pi,
|
||||
'k': scipy.constants.k,
|
||||
@@ -37,65 +66,166 @@ default_variables = {'j': numpy.complex(0, 1),
|
||||
'q': scipy.constants.e
|
||||
}
|
||||
|
||||
log = logging.getLogger("mitx.courseware.capa")
|
||||
# We eliminated the following extreme suffixes:
|
||||
# P (1e15), E (1e18), Z (1e21), Y (1e24),
|
||||
# f (1e-15), a (1e-18), z (1e-21), y (1e-24)
|
||||
# since they're rarely used, and potentially
|
||||
# confusing. They may also conflict with variables if we ever allow e.g.
|
||||
# 5R instead of 5*R
|
||||
SUFFIXES = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9, 'T': 1e12,
|
||||
'c': 1e-2, 'm': 1e-3, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12}
|
||||
|
||||
|
||||
class UndefinedVariable(Exception):
|
||||
def raiseself(self):
|
||||
''' Helper so we can use inside of a lambda '''
|
||||
raise self
|
||||
|
||||
|
||||
general_whitespace = re.compile('[^\w]+')
|
||||
"""
|
||||
Used to indicate the student input of a variable, which was unused by the
|
||||
instructor.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def check_variables(string, variables):
|
||||
'''Confirm the only variables in string are defined.
|
||||
"""
|
||||
Confirm the only variables in string are defined.
|
||||
|
||||
Pyparsing uses a left-to-right parser, which makes the more
|
||||
Otherwise, raise an UndefinedVariable containing all bad variables.
|
||||
|
||||
Pyparsing uses a left-to-right parser, which makes a more
|
||||
elegant approach pretty hopeless.
|
||||
|
||||
achar = reduce(lambda a,b:a|b ,map(Literal,alphas)) # Any alphabetic character
|
||||
undefined_variable = achar + Word(alphanums)
|
||||
undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself())
|
||||
varnames = varnames | undefined_variable
|
||||
'''
|
||||
possible_variables = re.split(general_whitespace, string) # List of all alnums in string
|
||||
bad_variables = list()
|
||||
for v in possible_variables:
|
||||
if len(v) == 0:
|
||||
"""
|
||||
general_whitespace = re.compile('[^\\w]+')
|
||||
# List of all alnums in string
|
||||
possible_variables = re.split(general_whitespace, string)
|
||||
bad_variables = []
|
||||
for var in possible_variables:
|
||||
if len(var) == 0:
|
||||
continue
|
||||
if v[0] <= '9' and '0' <= 'v': # Skip things that begin with numbers
|
||||
if var[0].isdigit(): # Skip things that begin with numbers
|
||||
continue
|
||||
if v not in variables:
|
||||
bad_variables.append(v)
|
||||
if var not in variables:
|
||||
bad_variables.append(var)
|
||||
if len(bad_variables) > 0:
|
||||
raise UndefinedVariable(' '.join(bad_variables))
|
||||
|
||||
|
||||
def lower_dict(input_dict):
|
||||
"""
|
||||
takes each key in the dict and makes it lowercase, still mapping to the
|
||||
same value.
|
||||
|
||||
keep in mind that it is possible (but not useful?) to define different
|
||||
variables that have the same lowercase representation. It would be hard to
|
||||
tell which is used in the final dict and which isn't.
|
||||
"""
|
||||
return {k.lower(): v for k, v in input_dict.iteritems()}
|
||||
|
||||
|
||||
# The following few functions define parse actions, which are run on lists of
|
||||
# results from each parse component. They convert the strings and (previously
|
||||
# calculated) numbers into the number that component represents.
|
||||
|
||||
def super_float(text):
|
||||
"""
|
||||
Like float, but with si extensions. 1k goes to 1000
|
||||
"""
|
||||
if text[-1] in SUFFIXES:
|
||||
return float(text[:-1]) * SUFFIXES[text[-1]]
|
||||
else:
|
||||
return float(text)
|
||||
|
||||
|
||||
def number_parse_action(parse_result):
|
||||
"""
|
||||
Create a float out of its string parts
|
||||
|
||||
e.g. [ '7', '.', '13' ] -> [ 7.13 ]
|
||||
Calls super_float above
|
||||
"""
|
||||
return super_float("".join(parse_result))
|
||||
|
||||
|
||||
def exp_parse_action(parse_result):
|
||||
"""
|
||||
Take a list of numbers and exponentiate them, right to left
|
||||
|
||||
e.g. [ 3, 2, 3 ] (which is 3^2^3 = 3^(2^3)) -> 6561
|
||||
"""
|
||||
# pyparsing.ParseResults doesn't play well with reverse()
|
||||
parse_result = reversed(parse_result)
|
||||
# the result of an exponentiation is called a power
|
||||
power = reduce(lambda a, b: b ** a, parse_result)
|
||||
return power
|
||||
|
||||
|
||||
def parallel(parse_result):
|
||||
"""
|
||||
Compute numbers according to the parallel resistors operator
|
||||
|
||||
BTW it is commutative. Its formula is given by
|
||||
out = 1 / (1/in1 + 1/in2 + ...)
|
||||
e.g. [ 1, 2 ] => 2/3
|
||||
|
||||
Return NaN if there is a zero among the inputs
|
||||
"""
|
||||
# convert from pyparsing.ParseResults, which doesn't support '0 in parse_result'
|
||||
parse_result = parse_result.asList()
|
||||
if len(parse_result) == 1:
|
||||
return parse_result[0]
|
||||
if 0 in parse_result:
|
||||
return float('nan')
|
||||
reciprocals = [1. / e for e in parse_result]
|
||||
return 1. / sum(reciprocals)
|
||||
|
||||
|
||||
def sum_parse_action(parse_result):
|
||||
"""
|
||||
Add the inputs
|
||||
|
||||
[ 1, '+', 2, '-', 3 ] -> 0
|
||||
|
||||
Allow a leading + or -
|
||||
"""
|
||||
total = 0.0
|
||||
current_op = operator.add
|
||||
for token in parse_result:
|
||||
if token is '+':
|
||||
current_op = operator.add
|
||||
elif token is '-':
|
||||
current_op = operator.sub
|
||||
else:
|
||||
total = current_op(total, token)
|
||||
return total
|
||||
|
||||
|
||||
def prod_parse_action(parse_result):
|
||||
"""
|
||||
Multiply the inputs
|
||||
|
||||
[ 1, '*', 2, '/', 3 ] => 0.66
|
||||
"""
|
||||
prod = 1.0
|
||||
current_op = operator.mul
|
||||
for token in parse_result:
|
||||
if token is '*':
|
||||
current_op = operator.mul
|
||||
elif token is '/':
|
||||
current_op = operator.truediv
|
||||
else:
|
||||
prod = current_op(prod, token)
|
||||
return prod
|
||||
|
||||
|
||||
def evaluator(variables, functions, string, cs=False):
|
||||
'''
|
||||
"""
|
||||
Evaluate an expression. Variables are passed as a dictionary
|
||||
from string to value. Unary functions are passed as a dictionary
|
||||
from string to function. Variables must be floats.
|
||||
cs: Case sensitive
|
||||
|
||||
TODO: Fix it so we can pass integers and complex numbers in variables dict
|
||||
'''
|
||||
# log.debug("variables: {0}".format(variables))
|
||||
# log.debug("functions: {0}".format(functions))
|
||||
# log.debug("string: {0}".format(string))
|
||||
|
||||
def lower_dict(d):
|
||||
return dict([(k.lower(), d[k]) for k in d])
|
||||
|
||||
all_variables = copy.copy(default_variables)
|
||||
all_functions = copy.copy(default_functions)
|
||||
|
||||
if not cs:
|
||||
all_variables = lower_dict(all_variables)
|
||||
all_functions = lower_dict(all_functions)
|
||||
"""
|
||||
|
||||
all_variables = copy.copy(DEFAULT_VARIABLES)
|
||||
all_functions = copy.copy(DEFAULT_FUNCTIONS)
|
||||
all_variables.update(variables)
|
||||
all_functions.update(functions)
|
||||
|
||||
@@ -113,122 +243,59 @@ def evaluator(variables, functions, string, cs=False):
|
||||
if string.strip() == "":
|
||||
return float('nan')
|
||||
|
||||
ops = {"^": operator.pow,
|
||||
"*": operator.mul,
|
||||
"/": operator.truediv,
|
||||
"+": operator.add,
|
||||
"-": operator.sub,
|
||||
}
|
||||
# We eliminated extreme ones, since they're rarely used, and potentially
|
||||
# confusing. They may also conflict with variables if we ever allow e.g.
|
||||
# 5R instead of 5*R
|
||||
suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9,
|
||||
'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
|
||||
'c': 1e-2, 'm': 1e-3, 'u': 1e-6,
|
||||
'n': 1e-9, 'p': 1e-12} # ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
|
||||
|
||||
def super_float(text):
|
||||
''' Like float, but with si extensions. 1k goes to 1000'''
|
||||
if text[-1] in suffixes:
|
||||
return float(text[:-1]) * suffixes[text[-1]]
|
||||
else:
|
||||
return float(text)
|
||||
|
||||
def number_parse_action(x): # [ '7' ] -> [ 7 ]
|
||||
return [super_float("".join(x))]
|
||||
|
||||
def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512
|
||||
x = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^
|
||||
x.reverse()
|
||||
x = reduce(lambda a, b: b ** a, x)
|
||||
return x
|
||||
|
||||
def parallel(x): # Parallel resistors [ 1 2 ] => 2/3
|
||||
# convert from pyparsing.ParseResults, which doesn't support '0 in x'
|
||||
x = list(x)
|
||||
if len(x) == 1:
|
||||
return x[0]
|
||||
if 0 in x:
|
||||
return float('nan')
|
||||
x = [1. / e for e in x if isinstance(e, numbers.Number)] # Ignore ||
|
||||
return 1. / sum(x)
|
||||
|
||||
def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0
|
||||
total = 0.0
|
||||
op = ops['+']
|
||||
for e in x:
|
||||
if e in set('+-'):
|
||||
op = ops[e]
|
||||
else:
|
||||
total = op(total, e)
|
||||
return total
|
||||
|
||||
def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66
|
||||
prod = 1.0
|
||||
op = ops['*']
|
||||
for e in x:
|
||||
if e in set('*/'):
|
||||
op = ops[e]
|
||||
else:
|
||||
prod = op(prod, e)
|
||||
return prod
|
||||
|
||||
def func_parse_action(x):
|
||||
return [all_functions[x[0]](x[1])]
|
||||
|
||||
# SI suffixes and percent
|
||||
number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch())
|
||||
(dot, minus, plus, times, div, lpar, rpar, exp) = map(Literal, ".-+*/()^")
|
||||
number_suffix = MatchFirst([Literal(k) for k in SUFFIXES.keys()])
|
||||
plus_minus = Literal('+') | Literal('-')
|
||||
times_div = Literal('*') | Literal('/')
|
||||
|
||||
number_part = Word(nums)
|
||||
|
||||
# 0.33 or 7 or .34 or 16.
|
||||
inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part)
|
||||
# by default pyparsing allows spaces between tokens--Combine prevents that
|
||||
inner_number = Combine(inner_number)
|
||||
|
||||
# 0.33k or -17
|
||||
number = (Optional(minus | plus) + inner_number
|
||||
+ Optional(CaselessLiteral("E") + Optional((plus | minus)) + number_part)
|
||||
number = (inner_number
|
||||
+ Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part)
|
||||
+ Optional(number_suffix))
|
||||
number = number.setParseAction(number_parse_action) # Convert to number
|
||||
number.setParseAction(number_parse_action) # Convert to number
|
||||
|
||||
# Predefine recursive variables
|
||||
expr = Forward()
|
||||
factor = Forward()
|
||||
|
||||
def sreduce(f, l):
|
||||
''' Same as reduce, but handle len 1 and len 0 lists sensibly '''
|
||||
if len(l) == 0:
|
||||
return NoMatch()
|
||||
if len(l) == 1:
|
||||
return l[0]
|
||||
return reduce(f, l)
|
||||
# Handle variables passed in.
|
||||
# E.g. if we have {'R':0.5}, we make the substitution.
|
||||
# We sort the list so that var names (like "e2") match before
|
||||
# mathematical constants (like "e"). This is kind of a hack.
|
||||
all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True)
|
||||
varnames = MatchFirst([CasedLiteral(k) for k in all_variables_keys])
|
||||
varnames.setParseAction(
|
||||
lambda x: [all_variables[k] for k in x]
|
||||
)
|
||||
|
||||
# Handle variables passed in. E.g. if we have {'R':0.5}, we make the substitution.
|
||||
# Special case for no variables because of how we understand PyParsing is put together
|
||||
if len(all_variables) > 0:
|
||||
# We sort the list so that var names (like "e2") match before
|
||||
# mathematical constants (like "e"). This is kind of a hack.
|
||||
all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True)
|
||||
varnames = sreduce(lambda x, y: x | y, map(lambda x: CasedLiteral(x), all_variables_keys))
|
||||
varnames.setParseAction(lambda x: map(lambda y: all_variables[y], x))
|
||||
else:
|
||||
varnames = NoMatch()
|
||||
# if all_variables were empty, then pyparsing wants
|
||||
# varnames = NoMatch()
|
||||
# this is not the case, as all_variables contains the defaults
|
||||
|
||||
# Same thing for functions.
|
||||
if len(all_functions) > 0:
|
||||
funcnames = sreduce(lambda x, y: x | y,
|
||||
map(lambda x: CasedLiteral(x), all_functions.keys()))
|
||||
function = funcnames + lpar.suppress() + expr + rpar.suppress()
|
||||
function.setParseAction(func_parse_action)
|
||||
else:
|
||||
function = NoMatch()
|
||||
all_functions_keys = sorted(all_functions.keys(), key=len, reverse=True)
|
||||
funcnames = MatchFirst([CasedLiteral(k) for k in all_functions_keys])
|
||||
function = funcnames + Suppress("(") + expr + Suppress(")")
|
||||
function.setParseAction(
|
||||
lambda x: [all_functions[x[0]](x[1])]
|
||||
)
|
||||
|
||||
atom = number | function | varnames | lpar + expr + rpar
|
||||
factor << (atom + ZeroOrMore(exp + atom)).setParseAction(exp_parse_action) # 7^6
|
||||
paritem = factor + ZeroOrMore(Literal('||') + factor) # 5k || 4k
|
||||
paritem = paritem.setParseAction(parallel)
|
||||
term = paritem + ZeroOrMore((times | div) + paritem) # 7 * 5 / 4 - 3
|
||||
term = term.setParseAction(prod_parse_action)
|
||||
expr << Optional((plus | minus)) + term + ZeroOrMore((plus | minus) + term) # -5 + 4 - 3
|
||||
expr = expr.setParseAction(sum_parse_action)
|
||||
atom = number | function | varnames | Suppress("(") + expr + Suppress(")")
|
||||
|
||||
# Do the following in the correct order to preserve order of operation
|
||||
pow_term = atom + ZeroOrMore(Suppress("^") + atom)
|
||||
pow_term.setParseAction(exp_parse_action) # 7^6
|
||||
par_term = pow_term + ZeroOrMore(Suppress('||') + pow_term) # 5k || 4k
|
||||
par_term.setParseAction(parallel)
|
||||
prod_term = par_term + ZeroOrMore(times_div + par_term) # 7 * 5 / 4 - 3
|
||||
prod_term.setParseAction(prod_parse_action)
|
||||
sum_term = Optional(plus_minus) + prod_term + ZeroOrMore(plus_minus + prod_term) # -5 + 4 - 3
|
||||
sum_term.setParseAction(sum_parse_action)
|
||||
expr << sum_term # finish the recursion
|
||||
return (expr + stringEnd).parseString(string)[0]
|
||||
|
||||
99
common/lib/calc/calcfunctions.py
Normal file
99
common/lib/calc/calcfunctions.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Provide the mathematical functions that numpy doesn't.
|
||||
|
||||
Specifically, the secant/cosecant/cotangents and their inverses and
|
||||
hyperbolic counterparts
|
||||
"""
|
||||
import numpy
|
||||
|
||||
|
||||
# Normal Trig
|
||||
def sec(arg):
|
||||
"""
|
||||
Secant
|
||||
"""
|
||||
return 1 / numpy.cos(arg)
|
||||
|
||||
|
||||
def csc(arg):
|
||||
"""
|
||||
Cosecant
|
||||
"""
|
||||
return 1 / numpy.sin(arg)
|
||||
|
||||
|
||||
def cot(arg):
|
||||
"""
|
||||
Cotangent
|
||||
"""
|
||||
return 1 / numpy.tan(arg)
|
||||
|
||||
|
||||
# Inverse Trig
|
||||
# http://en.wikipedia.org/wiki/Inverse_trigonometric_functions#Relationships_among_the_inverse_trigonometric_functions
|
||||
def arcsec(val):
|
||||
"""
|
||||
Inverse secant
|
||||
"""
|
||||
return numpy.arccos(1. / val)
|
||||
|
||||
|
||||
def arccsc(val):
|
||||
"""
|
||||
Inverse cosecant
|
||||
"""
|
||||
return numpy.arcsin(1. / val)
|
||||
|
||||
|
||||
def arccot(val):
|
||||
"""
|
||||
Inverse cotangent
|
||||
"""
|
||||
if numpy.real(val) < 0:
|
||||
return -numpy.pi / 2 - numpy.arctan(val)
|
||||
else:
|
||||
return numpy.pi / 2 - numpy.arctan(val)
|
||||
|
||||
|
||||
# Hyperbolic Trig
|
||||
def sech(arg):
|
||||
"""
|
||||
Hyperbolic secant
|
||||
"""
|
||||
return 1 / numpy.cosh(arg)
|
||||
|
||||
|
||||
def csch(arg):
|
||||
"""
|
||||
Hyperbolic cosecant
|
||||
"""
|
||||
return 1 / numpy.sinh(arg)
|
||||
|
||||
|
||||
def coth(arg):
|
||||
"""
|
||||
Hyperbolic cotangent
|
||||
"""
|
||||
return 1 / numpy.tanh(arg)
|
||||
|
||||
|
||||
# And their inverses
|
||||
def arcsech(val):
|
||||
"""
|
||||
Inverse hyperbolic secant
|
||||
"""
|
||||
return numpy.arccosh(1. / val)
|
||||
|
||||
|
||||
def arccsch(val):
|
||||
"""
|
||||
Inverse hyperbolic cosecant
|
||||
"""
|
||||
return numpy.arcsinh(1. / val)
|
||||
|
||||
|
||||
def arccoth(val):
|
||||
"""
|
||||
Inverse hyperbolic cotangent
|
||||
"""
|
||||
return numpy.arctanh(1. / val)
|
||||
@@ -194,6 +194,105 @@ class EvaluatorTest(unittest.TestCase):
|
||||
arctan_angles = arcsin_angles
|
||||
self.assert_function_values('arctan', arctan_inputs, arctan_angles)
|
||||
|
||||
def test_reciprocal_trig_functions(self):
|
||||
"""
|
||||
Test the reciprocal trig functions provided in calc.py
|
||||
|
||||
which are: sec, csc, cot, arcsec, arccsc, arccot
|
||||
"""
|
||||
angles = ['-pi/4', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', '1 + j']
|
||||
sec_values = [1.414, 1.155, 1.236, -1.414, 1.414, 0.498 + 0.591j]
|
||||
csc_values = [-1.414, 2, 1.701, -1.414, 1.414, 0.622 - 0.304j]
|
||||
cot_values = [-1, 1.732, 1.376, 1, 1, 0.218 - 0.868j]
|
||||
|
||||
self.assert_function_values('sec', angles, sec_values)
|
||||
self.assert_function_values('csc', angles, csc_values)
|
||||
self.assert_function_values('cot', angles, cot_values)
|
||||
|
||||
arcsec_inputs = ['1.1547', '1.2361', '2', '-2', '-1.4142', '0.4983+0.5911*j']
|
||||
arcsec_angles = [0.524, 0.628, 1.047, 2.094, 2.356, 1 + 1j]
|
||||
self.assert_function_values('arcsec', arcsec_inputs, arcsec_angles)
|
||||
|
||||
arccsc_inputs = ['-1.1547', '-1.4142', '2', '1.7013', '1.1547', '0.6215-0.3039*j']
|
||||
arccsc_angles = [-1.047, -0.785, 0.524, 0.628, 1.047, 1 + 1j]
|
||||
self.assert_function_values('arccsc', arccsc_inputs, arccsc_angles)
|
||||
|
||||
# Has the same range as arccsc
|
||||
arccot_inputs = ['-0.5774', '-1', '1.7321', '1.3764', '0.5774', '(0.2176-0.868*j)']
|
||||
arccot_angles = arccsc_angles
|
||||
self.assert_function_values('arccot', arccot_inputs, arccot_angles)
|
||||
|
||||
def test_hyperbolic_functions(self):
|
||||
"""
|
||||
Test the hyperbolic functions
|
||||
|
||||
which are: sinh, cosh, tanh, sech, csch, coth
|
||||
"""
|
||||
inputs = ['0', '0.5', '1', '2', '1+j']
|
||||
neg_inputs = ['0', '-0.5', '-1', '-2', '-1-j']
|
||||
negate = lambda x: [-k for k in x]
|
||||
|
||||
# sinh is odd
|
||||
sinh_vals = [0, 0.521, 1.175, 3.627, 0.635 + 1.298j]
|
||||
self.assert_function_values('sinh', inputs, sinh_vals)
|
||||
self.assert_function_values('sinh', neg_inputs, negate(sinh_vals))
|
||||
|
||||
# cosh is even - do not negate
|
||||
cosh_vals = [1, 1.128, 1.543, 3.762, 0.834 + 0.989j]
|
||||
self.assert_function_values('cosh', inputs, cosh_vals)
|
||||
self.assert_function_values('cosh', neg_inputs, cosh_vals)
|
||||
|
||||
# tanh is odd
|
||||
tanh_vals = [0, 0.462, 0.762, 0.964, 1.084 + 0.272j]
|
||||
self.assert_function_values('tanh', inputs, tanh_vals)
|
||||
self.assert_function_values('tanh', neg_inputs, negate(tanh_vals))
|
||||
|
||||
# sech is even - do not negate
|
||||
sech_vals = [1, 0.887, 0.648, 0.266, 0.498 - 0.591j]
|
||||
self.assert_function_values('sech', inputs, sech_vals)
|
||||
self.assert_function_values('sech', neg_inputs, sech_vals)
|
||||
|
||||
# the following functions do not have 0 in their domain
|
||||
inputs = inputs[1:]
|
||||
neg_inputs = neg_inputs[1:]
|
||||
|
||||
# csch is odd
|
||||
csch_vals = [1.919, 0.851, 0.276, 0.304 - 0.622j]
|
||||
self.assert_function_values('csch', inputs, csch_vals)
|
||||
self.assert_function_values('csch', neg_inputs, negate(csch_vals))
|
||||
|
||||
# coth is odd
|
||||
coth_vals = [2.164, 1.313, 1.037, 0.868 - 0.218j]
|
||||
self.assert_function_values('coth', inputs, coth_vals)
|
||||
self.assert_function_values('coth', neg_inputs, negate(coth_vals))
|
||||
|
||||
def test_hyperbolic_inverses(self):
|
||||
"""
|
||||
Test the inverse hyperbolic functions
|
||||
|
||||
which are of the form arc[X]h
|
||||
"""
|
||||
results = [0, 0.5, 1, 2, 1 + 1j]
|
||||
|
||||
sinh_vals = ['0', '0.5211', '1.1752', '3.6269', '0.635+1.2985*j']
|
||||
self.assert_function_values('arcsinh', sinh_vals, results)
|
||||
|
||||
cosh_vals = ['1', '1.1276', '1.5431', '3.7622', '0.8337+0.9889*j']
|
||||
self.assert_function_values('arccosh', cosh_vals, results)
|
||||
|
||||
tanh_vals = ['0', '0.4621', '0.7616', '0.964', '1.0839+0.2718*j']
|
||||
self.assert_function_values('arctanh', tanh_vals, results)
|
||||
|
||||
sech_vals = ['1.0', '0.8868', '0.6481', '0.2658', '0.4983-0.5911*j']
|
||||
self.assert_function_values('arcsech', sech_vals, results)
|
||||
|
||||
results = results[1:]
|
||||
csch_vals = ['1.919', '0.8509', '0.2757', '0.3039-0.6215*j']
|
||||
self.assert_function_values('arccsch', csch_vals, results)
|
||||
|
||||
coth_vals = ['2.164', '1.313', '1.0373', '0.868-0.2176*j']
|
||||
self.assert_function_values('arccoth', coth_vals, results)
|
||||
|
||||
def test_other_functions(self):
|
||||
"""
|
||||
Test the non-trig functions provided in calc.py
|
||||
|
||||
@@ -1738,6 +1738,7 @@ class FormulaResponse(LoncapaResponse):
|
||||
student_variables = dict()
|
||||
# ranges give numerical ranges for testing
|
||||
for var in ranges:
|
||||
# TODO: allow specified ranges (i.e. integers and complex numbers) for random variables
|
||||
value = random.uniform(*ranges[var])
|
||||
instructor_variables[str(var)] = value
|
||||
student_variables[str(var)] = value
|
||||
|
||||
@@ -6,7 +6,7 @@ from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xmodule.exceptions import InvalidDefinitionError
|
||||
from xblock.core import String, Scope, Object
|
||||
from xblock.core import String, Scope, Dict
|
||||
|
||||
DEFAULT = "_DEFAULT_GROUP"
|
||||
|
||||
@@ -32,9 +32,9 @@ def group_from_value(groups, v):
|
||||
|
||||
|
||||
class ABTestFields(object):
|
||||
group_portions = Object(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content)
|
||||
group_assignments = Object(help="What group this user belongs to", scope=Scope.preferences, default={})
|
||||
group_content = Object(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
|
||||
group_portions = Dict(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content)
|
||||
group_assignments = Dict(help="What group this user belongs to", scope=Scope.preferences, default={})
|
||||
group_content = Dict(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
|
||||
experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content)
|
||||
has_children = True
|
||||
|
||||
|
||||
@@ -125,6 +125,5 @@ class AnnotatableModule(AnnotatableFields, XModule):
|
||||
|
||||
class AnnotatableDescriptor(AnnotatableFields, RawDescriptor):
|
||||
module_class = AnnotatableModule
|
||||
stores_state = True
|
||||
template_dir_name = "annotatable"
|
||||
mako_template = "widgets/raw-edit.html"
|
||||
|
||||
@@ -18,8 +18,8 @@ from .progress import Progress
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from xblock.core import Scope, String, Boolean, Object
|
||||
from .fields import Timedelta, Date, StringyInteger, StringyFloat
|
||||
from xblock.core import Scope, String, Boolean, Dict, Integer, Float
|
||||
from .fields import Timedelta, Date
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
@@ -65,8 +65,8 @@ class ComplexEncoder(json.JSONEncoder):
|
||||
|
||||
|
||||
class CapaFields(object):
|
||||
attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state)
|
||||
max_attempts = StringyInteger(
|
||||
attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state)
|
||||
max_attempts = Integer(
|
||||
display_name="Maximum Attempts",
|
||||
help="Defines the number of times a student can try to answer this problem. If the value is not set, infinite attempts are allowed.",
|
||||
values={"min": 0}, scope=Scope.settings
|
||||
@@ -95,12 +95,12 @@ class CapaFields(object):
|
||||
{"display_name": "Per Student", "value": "per_student"}]
|
||||
)
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={})
|
||||
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state)
|
||||
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state)
|
||||
correct_map = Dict(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={})
|
||||
input_state = Dict(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state)
|
||||
student_answers = Dict(help="Dictionary with the current student responses", scope=Scope.user_state)
|
||||
done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state)
|
||||
seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state)
|
||||
weight = StringyFloat(
|
||||
seed = Integer(help="Random seed for this student", scope=Scope.user_state)
|
||||
weight = Float(
|
||||
display_name="Problem Weight",
|
||||
help="Defines the number of points each problem is worth. If the value is not set, each response field in the problem is worth one point.",
|
||||
values={"min": 0, "step": .1},
|
||||
@@ -315,7 +315,7 @@ class CapaModule(CapaFields, XModule):
|
||||
# If the user has forced the save button to display,
|
||||
# then show it as long as the problem is not closed
|
||||
# (past due / too many attempts)
|
||||
if self.force_save_button == "true":
|
||||
if self.force_save_button:
|
||||
return not self.closed()
|
||||
else:
|
||||
is_survey_question = (self.max_attempts == 0)
|
||||
@@ -902,7 +902,6 @@ class CapaDescriptor(CapaFields, RawDescriptor):
|
||||
|
||||
module_class = CapaModule
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
template_dir_name = 'problem'
|
||||
mako_template = "widgets/problem-edit.html"
|
||||
|
||||
@@ -5,10 +5,10 @@ from pkg_resources import resource_string
|
||||
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from .x_module import XModule
|
||||
from xblock.core import Integer, Scope, String, List
|
||||
from xblock.core import Integer, Scope, String, List, Float, Boolean
|
||||
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
|
||||
from collections import namedtuple
|
||||
from .fields import Date, StringyFloat, StringyInteger, StringyBoolean
|
||||
from .fields import Date
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
@@ -53,27 +53,27 @@ class CombinedOpenEndedFields(object):
|
||||
help="This name appears in the horizontal navigation at the top of the page.",
|
||||
default="Open Ended Grading", scope=Scope.settings
|
||||
)
|
||||
current_task_number = StringyInteger(help="Current task that the student is on.", default=0, scope=Scope.user_state)
|
||||
current_task_number = Integer(help="Current task that the student is on.", default=0, scope=Scope.user_state)
|
||||
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.user_state)
|
||||
state = String(help="Which step within the current task that the student is on.", default="initial",
|
||||
scope=Scope.user_state)
|
||||
student_attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0,
|
||||
student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0,
|
||||
scope=Scope.user_state)
|
||||
ready_to_reset = StringyBoolean(
|
||||
ready_to_reset = Boolean(
|
||||
help="If the problem is ready to be reset or not.", default=False,
|
||||
scope=Scope.user_state
|
||||
)
|
||||
attempts = StringyInteger(
|
||||
attempts = Integer(
|
||||
display_name="Maximum Attempts",
|
||||
help="The number of times the student can try to answer this problem.", default=1,
|
||||
scope=Scope.settings, values = {"min" : 1 }
|
||||
)
|
||||
is_graded = StringyBoolean(display_name="Graded", help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
|
||||
accept_file_upload = StringyBoolean(
|
||||
is_graded = Boolean(display_name="Graded", help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
|
||||
accept_file_upload = Boolean(
|
||||
display_name="Allow File Uploads",
|
||||
help="Whether or not the student can submit files as a response.", default=False, scope=Scope.settings
|
||||
)
|
||||
skip_spelling_checks = StringyBoolean(
|
||||
skip_spelling_checks = Boolean(
|
||||
display_name="Disable Quality Filter",
|
||||
help="If False, the Quality Filter is enabled and submissions with poor spelling, short length, or poor grammar will not be peer reviewed.",
|
||||
default=False, scope=Scope.settings
|
||||
@@ -86,7 +86,7 @@ class CombinedOpenEndedFields(object):
|
||||
)
|
||||
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
weight = StringyFloat(
|
||||
weight = Float(
|
||||
display_name="Problem Weight",
|
||||
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
|
||||
scope=Scope.settings, values = {"min" : 0 , "step": ".1"}
|
||||
@@ -239,7 +239,6 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
|
||||
mako_template = "widgets/open-ended-edit.html"
|
||||
module_class = CombinedOpenEndedModule
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
always_recalculate_grades = True
|
||||
template_dir_name = "combinedopenended"
|
||||
|
||||
@@ -92,7 +92,7 @@ class ConditionalModule(ConditionalFields, XModule):
|
||||
if xml_value and self.required_modules:
|
||||
for module in self.required_modules:
|
||||
if not hasattr(module, attr_name):
|
||||
# We don't throw an exception here because it is possible for
|
||||
# We don't throw an exception here because it is possible for
|
||||
# the descriptor of a required module to have a property but
|
||||
# for the resulting module to be a (flavor of) ErrorModule.
|
||||
# So just log and return false.
|
||||
@@ -161,7 +161,6 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor):
|
||||
|
||||
filename_extension = "xml"
|
||||
|
||||
stores_state = True
|
||||
has_score = False
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -15,7 +15,7 @@ from xmodule.util.decorators import lazyproperty
|
||||
from xmodule.graders import grader_from_conf
|
||||
import json
|
||||
|
||||
from xblock.core import Scope, List, String, Object, Boolean
|
||||
from xblock.core import Scope, List, String, Dict, Boolean
|
||||
from .fields import Date
|
||||
from django.utils.timezone import UTC
|
||||
from xmodule.util import date_utils
|
||||
@@ -154,25 +154,25 @@ class CourseFields(object):
|
||||
start = Date(help="Start time when this module is visible", scope=Scope.settings)
|
||||
end = Date(help="Date that this class ends", scope=Scope.settings)
|
||||
advertised_start = String(help="Date that this course is advertised to start", scope=Scope.settings)
|
||||
grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content)
|
||||
grading_policy = Dict(help="Grading policy definition for this class", scope=Scope.content)
|
||||
show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings)
|
||||
display_name = String(help="Display name for this module", scope=Scope.settings)
|
||||
tabs = List(help="List of tabs to enable in this course", scope=Scope.settings)
|
||||
end_of_course_survey_url = String(help="Url for the end-of-course survey", scope=Scope.settings)
|
||||
discussion_blackouts = List(help="List of pairs of start/end dates for discussion blackouts", scope=Scope.settings)
|
||||
discussion_topics = Object(
|
||||
discussion_topics = Dict(
|
||||
help="Map of topics names to ids",
|
||||
scope=Scope.settings
|
||||
)
|
||||
testcenter_info = Object(help="Dictionary of Test Center info", scope=Scope.settings)
|
||||
testcenter_info = Dict(help="Dictionary of Test Center info", scope=Scope.settings)
|
||||
announcement = Date(help="Date this course is announced", scope=Scope.settings)
|
||||
cohort_config = Object(help="Dictionary defining cohort configuration", scope=Scope.settings)
|
||||
cohort_config = Dict(help="Dictionary defining cohort configuration", scope=Scope.settings)
|
||||
is_new = Boolean(help="Whether this course should be flagged as new", scope=Scope.settings)
|
||||
no_grade = Boolean(help="True if this course isn't graded", default=False, scope=Scope.settings)
|
||||
disable_progress_graph = Boolean(help="True if this course shouldn't display the progress graph", default=False, scope=Scope.settings)
|
||||
pdf_textbooks = List(help="List of dictionaries containing pdf_textbook configuration", scope=Scope.settings)
|
||||
html_textbooks = List(help="List of dictionaries containing html_textbook configuration", scope=Scope.settings)
|
||||
remote_gradebook = Object(scope=Scope.settings)
|
||||
remote_gradebook = Dict(scope=Scope.settings)
|
||||
allow_anonymous = Boolean(scope=Scope.settings, default=True)
|
||||
allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False)
|
||||
advanced_modules = List(help="Beta modules used in your course", scope=Scope.settings)
|
||||
|
||||
@@ -6,7 +6,6 @@ from xblock.core import ModelType
|
||||
import datetime
|
||||
import dateutil.parser
|
||||
|
||||
from xblock.core import Integer, Float, Boolean
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -93,42 +92,3 @@ class Timedelta(ModelType):
|
||||
if cur_value > 0:
|
||||
values.append("%d %s" % (cur_value, attr))
|
||||
return ' '.join(values)
|
||||
|
||||
|
||||
class StringyInteger(Integer):
|
||||
"""
|
||||
A model type that converts from strings to integers when reading from json.
|
||||
If value does not parse as an int, returns None.
|
||||
"""
|
||||
def from_json(self, value):
|
||||
try:
|
||||
return int(value)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
class StringyFloat(Float):
|
||||
"""
|
||||
A model type that converts from string to floats when reading from json.
|
||||
If value does not parse as a float, returns None.
|
||||
"""
|
||||
def from_json(self, value):
|
||||
try:
|
||||
return float(value)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
class StringyBoolean(Boolean):
|
||||
"""
|
||||
Reads strings from JSON as booleans.
|
||||
|
||||
If the string is 'true' (case insensitive), then return True,
|
||||
otherwise False.
|
||||
|
||||
JSON values that aren't strings are returned as-is.
|
||||
"""
|
||||
def from_json(self, value):
|
||||
if isinstance(value, basestring):
|
||||
return value.lower() == 'true'
|
||||
return value
|
||||
|
||||
@@ -183,7 +183,6 @@ class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
|
||||
module_class = FolditModule
|
||||
filename_extension = "xml"
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
template_dir_name = "foldit"
|
||||
|
||||
|
||||
@@ -140,12 +140,16 @@ class @VideoCaptionAlpha extends SubviewAlpha
|
||||
|
||||
hideCaptions: (hide_captions) =>
|
||||
if hide_captions
|
||||
type = 'hide_transcript'
|
||||
@$('.hide-subtitles').attr('title', 'Turn on captions')
|
||||
@el.addClass('closed')
|
||||
else
|
||||
type = 'show_transcript'
|
||||
@$('.hide-subtitles').attr('title', 'Turn off captions')
|
||||
@el.removeClass('closed')
|
||||
@scrollCaption()
|
||||
@video.log type,
|
||||
currentTime: @player.currentTime
|
||||
$.cookie('hide_captions', hide_captions, expires: 3650, path: '/')
|
||||
|
||||
captionHeight: ->
|
||||
|
||||
@@ -45,6 +45,8 @@ class @VideoPlayerAlpha extends SubviewAlpha
|
||||
if @video.show_captions is true
|
||||
@caption = new VideoCaptionAlpha
|
||||
el: @el
|
||||
video: @video
|
||||
player: @
|
||||
youtubeId: @video.youtubeId('1.0')
|
||||
currentSpeed: @currentSpeed()
|
||||
captionAssetPath: @video.caption_asset_path
|
||||
@@ -66,7 +68,16 @@ class @VideoPlayerAlpha extends SubviewAlpha
|
||||
if @video.end
|
||||
# work in AS3, not HMLT5. but iframe use AS3
|
||||
@playerVars.end = @video.end
|
||||
|
||||
# There is a bug which prevents YouTube API to correctly set the speed to 1.0 from another speed
|
||||
# in Firefox when in HTML5 mode. There is a fix which basically reloads the video at speed 1.0
|
||||
# when this change is requested (instead of simply requesting a speed change to 1.0). This has to
|
||||
# be done only when the video is being watched in Firefox. We need to figure out what browser is
|
||||
# currently executing this code.
|
||||
@video.isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1
|
||||
|
||||
if @video.videoType is 'html5'
|
||||
@video.playerType = 'browser'
|
||||
@player = new HTML5Video.Player @video.el,
|
||||
playerVars: @playerVars,
|
||||
videoSources: @video.html5Sources,
|
||||
@@ -79,6 +90,7 @@ class @VideoPlayerAlpha extends SubviewAlpha
|
||||
youTubeId = @video.videos['1.0']
|
||||
else
|
||||
youTubeId = @video.youtubeId()
|
||||
@video.playerType = 'youtube'
|
||||
@player = new YT.Player @video.id,
|
||||
playerVars: @playerVars
|
||||
videoId: youTubeId
|
||||
@@ -99,7 +111,7 @@ class @VideoPlayerAlpha extends SubviewAlpha
|
||||
@video.log 'load_video'
|
||||
if @video.videoType is 'html5'
|
||||
@player.setPlaybackRate @video.speed
|
||||
unless onTouchBasedDevice()
|
||||
if not onTouchBasedDevice() and $('.video:first').data('autoplay') is 'True'
|
||||
$('.video-load-complete:first').data('video').player.play()
|
||||
|
||||
onStateChange: (event) =>
|
||||
@@ -235,13 +247,18 @@ class @VideoPlayerAlpha extends SubviewAlpha
|
||||
if @video.videoType is 'youtube'
|
||||
if @video.show_captions is true
|
||||
@caption.currentSpeed = newSpeed
|
||||
if @video.videoType is 'html5'
|
||||
@player.setPlaybackRate newSpeed
|
||||
else if @video.videoType is 'youtube'
|
||||
|
||||
# We request the reloading of the video in the case when YouTube is in Flash player mode,
|
||||
# or when we are in Firefox, and the new speed is 1.0. The second case is necessary to
|
||||
# avoid the bug where in Firefox speed switching to 1.0 in HTML5 player mode is handled
|
||||
# incorrectly by YouTube API.
|
||||
if (@video.videoType is 'youtube') or ((@video.isFirefox) and (@video.playerType is 'youtube') and (newSpeed is '1.0'))
|
||||
if @isPlaying()
|
||||
@player.loadVideoById(@video.youtubeId(), @currentTime)
|
||||
else
|
||||
@player.cueVideoById(@video.youtubeId(), @currentTime)
|
||||
else if @video.videoType is 'html5'
|
||||
@player.setPlaybackRate newSpeed
|
||||
if @video.videoType is 'youtube'
|
||||
@updatePlayTime @currentTime
|
||||
|
||||
@@ -262,11 +279,15 @@ class @VideoPlayerAlpha extends SubviewAlpha
|
||||
toggleFullScreen: (event) =>
|
||||
event.preventDefault()
|
||||
if @el.hasClass('fullscreen')
|
||||
type = 'not_fullscreen'
|
||||
@$('.add-fullscreen').attr('title', 'Fill browser')
|
||||
@el.removeClass('fullscreen')
|
||||
else
|
||||
type = 'fullscreen'
|
||||
@el.addClass('fullscreen')
|
||||
@$('.add-fullscreen').attr('title', 'Exit fill browser')
|
||||
@video.log type,
|
||||
currentTime: @currentTime
|
||||
if @video.show_captions is true
|
||||
@caption.resize()
|
||||
|
||||
|
||||
@@ -823,7 +823,6 @@ class CombinedOpenEndedV1Descriptor():
|
||||
module_class = CombinedOpenEndedV1Module
|
||||
filename_extension = "xml"
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
template_dir_name = "combinedopenended"
|
||||
|
||||
|
||||
@@ -731,7 +731,6 @@ class OpenEndedDescriptor():
|
||||
module_class = OpenEndedModule
|
||||
filename_extension = "xml"
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
template_dir_name = "openended"
|
||||
|
||||
|
||||
@@ -286,7 +286,6 @@ class SelfAssessmentDescriptor():
|
||||
module_class = SelfAssessmentModule
|
||||
filename_extension = "xml"
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
template_dir_name = "selfassessment"
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ from .x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from .timeinfo import TimeInfo
|
||||
from xblock.core import Object, String, Scope
|
||||
from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean
|
||||
from xblock.core import Dict, String, Scope, Boolean, Integer, Float
|
||||
from xmodule.fields import Date
|
||||
|
||||
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
|
||||
from open_ended_grading_classes import combined_open_ended_rubric
|
||||
@@ -21,7 +21,6 @@ log = logging.getLogger(__name__)
|
||||
|
||||
USE_FOR_SINGLE_LOCATION = False
|
||||
LINK_TO_LOCATION = ""
|
||||
TRUE_DICT = [True, "True", "true", "TRUE"]
|
||||
MAX_SCORE = 1
|
||||
IS_GRADED = False
|
||||
|
||||
@@ -29,7 +28,7 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please
|
||||
|
||||
|
||||
class PeerGradingFields(object):
|
||||
use_for_single_location = StringyBoolean(
|
||||
use_for_single_location = Boolean(
|
||||
display_name="Show Single Problem",
|
||||
help='When True, only the single problem specified by "Link to Problem Location" is shown. '
|
||||
'When False, a panel is displayed with all problems available for peer grading.',
|
||||
@@ -40,22 +39,22 @@ class PeerGradingFields(object):
|
||||
help='The location of the problem being graded. Only used when "Show Single Problem" is True.',
|
||||
default=LINK_TO_LOCATION, scope=Scope.settings
|
||||
)
|
||||
is_graded = StringyBoolean(
|
||||
is_graded = Boolean(
|
||||
display_name="Graded",
|
||||
help='Defines whether the student gets credit for grading this problem. Only used when "Show Single Problem" is True.',
|
||||
default=IS_GRADED, scope=Scope.settings
|
||||
)
|
||||
due_date = Date(help="Due date that should be displayed.", default=None, scope=Scope.settings)
|
||||
grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings)
|
||||
max_grade = StringyInteger(
|
||||
max_grade = Integer(
|
||||
help="The maximum grade that a student can receive for this problem.", default=MAX_SCORE,
|
||||
scope=Scope.settings, values={"min": 0}
|
||||
)
|
||||
student_data_for_location = Object(
|
||||
student_data_for_location = Dict(
|
||||
help="Student data for a given peer grading problem.",
|
||||
scope=Scope.user_state
|
||||
)
|
||||
weight = StringyFloat(
|
||||
weight = Float(
|
||||
display_name="Problem Weight",
|
||||
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
|
||||
scope=Scope.settings, values={"min": 0, "step": ".1"}
|
||||
@@ -85,7 +84,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
|
||||
else:
|
||||
self.peer_gs = MockPeerGradingService()
|
||||
|
||||
if self.use_for_single_location in TRUE_DICT:
|
||||
if self.use_for_single_location:
|
||||
try:
|
||||
self.linked_problem = modulestore().get_instance(self.system.course_id, self.link_to_location)
|
||||
except:
|
||||
@@ -113,7 +112,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
|
||||
if not self.ajax_url.endswith("/"):
|
||||
self.ajax_url = self.ajax_url + "/"
|
||||
|
||||
# StringyInteger could return None, so keep this check.
|
||||
# Integer could return None, so keep this check.
|
||||
if not isinstance(self.max_grade, int):
|
||||
raise TypeError("max_grade needs to be an integer.")
|
||||
|
||||
@@ -147,7 +146,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
|
||||
"""
|
||||
if self.closed():
|
||||
return self.peer_grading_closed()
|
||||
if self.use_for_single_location not in TRUE_DICT:
|
||||
if not self.use_for_single_location:
|
||||
return self.peer_grading()
|
||||
else:
|
||||
return self.peer_grading_problem({'location': self.link_to_location})['html']
|
||||
@@ -204,7 +203,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
|
||||
'score': score,
|
||||
'total': max_score,
|
||||
}
|
||||
if self.use_for_single_location not in TRUE_DICT or self.is_graded not in TRUE_DICT:
|
||||
if not self.use_for_single_location or not self.is_graded:
|
||||
return score_dict
|
||||
|
||||
try:
|
||||
@@ -239,7 +238,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
|
||||
randomization, and 5/7 on another
|
||||
'''
|
||||
max_grade = None
|
||||
if self.use_for_single_location in TRUE_DICT and self.is_graded in TRUE_DICT:
|
||||
if self.use_for_single_location and self.is_graded:
|
||||
max_grade = self.max_grade
|
||||
return max_grade
|
||||
|
||||
@@ -557,7 +556,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
|
||||
Show individual problem interface
|
||||
'''
|
||||
if get is None or get.get('location') is None:
|
||||
if self.use_for_single_location not in TRUE_DICT:
|
||||
if not self.use_for_single_location:
|
||||
# This is an error case, because it must be set to use a single location to be called without get parameters
|
||||
# This is a dev_facing_error
|
||||
log.error(
|
||||
@@ -603,7 +602,6 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
|
||||
module_class = PeerGradingModule
|
||||
filename_extension = "xml"
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
always_recalculate_grades = True
|
||||
template_dir_name = "peer_grading"
|
||||
|
||||
@@ -19,7 +19,7 @@ from xmodule.x_module import XModule
|
||||
from xmodule.stringify import stringify_children
|
||||
from xmodule.mako_module import MakoModuleDescriptor
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xblock.core import Scope, String, Object, Boolean, List
|
||||
from xblock.core import Scope, String, Dict, Boolean, List
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,7 +30,7 @@ class PollFields(object):
|
||||
|
||||
voted = Boolean(help="Whether this student has voted on the poll", scope=Scope.user_state, default=False)
|
||||
poll_answer = String(help="Student answer", scope=Scope.user_state, default='')
|
||||
poll_answers = Object(help="All possible answers for the poll fro other students", scope=Scope.content)
|
||||
poll_answers = Dict(help="All possible answers for the poll fro other students", scope=Scope.content)
|
||||
|
||||
answers = List(help="Poll answers from xml", scope=Scope.content, default=[])
|
||||
question = String(help="Poll question", scope=Scope.content, default='')
|
||||
@@ -141,7 +141,6 @@ class PollDescriptor(PollFields, MakoModuleDescriptor, XmlDescriptor):
|
||||
|
||||
module_class = PollModule
|
||||
template_dir_name = 'poll'
|
||||
stores_state = True
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
|
||||
@@ -94,7 +94,6 @@ class RandomizeDescriptor(RandomizeFields, SequenceDescriptor):
|
||||
|
||||
filename_extension = "xml"
|
||||
|
||||
stores_state = True
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
|
||||
|
||||
@@ -121,8 +121,6 @@ class SequenceDescriptor(SequenceFields, MakoModuleDescriptor, XmlDescriptor):
|
||||
mako_template = 'widgets/sequence-edit.html'
|
||||
module_class = SequenceModule
|
||||
|
||||
stores_state = True # For remembering where in the sequence the student is
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/sequence/edit.coffee')]}
|
||||
js_module_name = "SequenceDescriptor"
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Tests for classes defined in fields.py."""
|
||||
import datetime
|
||||
import unittest
|
||||
from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean
|
||||
from django.utils.timezone import UTC
|
||||
from xmodule.fields import Date, Timedelta
|
||||
|
||||
|
||||
class DateTest(unittest.TestCase):
|
||||
date = Date()
|
||||
@@ -70,54 +71,22 @@ class DateTest(unittest.TestCase):
|
||||
"2012-12-31T23:00:01-01:00")
|
||||
|
||||
|
||||
class StringyIntegerTest(unittest.TestCase):
|
||||
def assertEquals(self, expected, arg):
|
||||
self.assertEqual(expected, StringyInteger().from_json(arg))
|
||||
class TimedeltaTest(unittest.TestCase):
|
||||
delta = Timedelta()
|
||||
|
||||
def test_integer(self):
|
||||
self.assertEquals(5, '5')
|
||||
self.assertEquals(0, '0')
|
||||
self.assertEquals(-1023, '-1023')
|
||||
def test_from_json(self):
|
||||
self.assertEqual(
|
||||
TimedeltaTest.delta.from_json('1 day 12 hours 59 minutes 59 seconds'),
|
||||
datetime.timedelta(days=1, hours=12, minutes=59, seconds=59)
|
||||
)
|
||||
|
||||
def test_none(self):
|
||||
self.assertEquals(None, None)
|
||||
self.assertEquals(None, 'abc')
|
||||
self.assertEquals(None, '[1]')
|
||||
self.assertEquals(None, '1.023')
|
||||
|
||||
|
||||
class StringyFloatTest(unittest.TestCase):
|
||||
|
||||
def assertEquals(self, expected, arg):
|
||||
self.assertEqual(expected, StringyFloat().from_json(arg))
|
||||
|
||||
def test_float(self):
|
||||
self.assertEquals(.23, '.23')
|
||||
self.assertEquals(5, '5')
|
||||
self.assertEquals(0, '0.0')
|
||||
self.assertEquals(-1023.22, '-1023.22')
|
||||
|
||||
def test_none(self):
|
||||
self.assertEquals(None, None)
|
||||
self.assertEquals(None, 'abc')
|
||||
self.assertEquals(None, '[1]')
|
||||
|
||||
|
||||
class StringyBooleanTest(unittest.TestCase):
|
||||
|
||||
def assertEquals(self, expected, arg):
|
||||
self.assertEqual(expected, StringyBoolean().from_json(arg))
|
||||
|
||||
def test_false(self):
|
||||
self.assertEquals(False, "false")
|
||||
self.assertEquals(False, "False")
|
||||
self.assertEquals(False, "")
|
||||
self.assertEquals(False, "hahahahah")
|
||||
|
||||
def test_true(self):
|
||||
self.assertEquals(True, "true")
|
||||
self.assertEquals(True, "TruE")
|
||||
|
||||
def test_pass_through(self):
|
||||
self.assertEquals(123, 123)
|
||||
self.assertEqual(
|
||||
TimedeltaTest.delta.from_json('1 day 46799 seconds'),
|
||||
datetime.timedelta(days=1, seconds=46799)
|
||||
)
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(
|
||||
'1 days 46799 seconds',
|
||||
TimedeltaTest.delta.to_json(datetime.timedelta(days=1, hours=12, minutes=59, seconds=59))
|
||||
)
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
#pylint: disable=C0111
|
||||
|
||||
from xmodule.x_module import XModuleFields
|
||||
from xblock.core import Scope, String, Object, Boolean
|
||||
from xmodule.fields import Date, StringyInteger, StringyFloat
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xblock.core import Scope, String, Dict, Boolean, Integer, Float, Any, List
|
||||
from xmodule.fields import Date, Timedelta
|
||||
from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field
|
||||
import unittest
|
||||
from .import test_system
|
||||
from nose.tools import assert_equals
|
||||
from mock import Mock
|
||||
|
||||
|
||||
@@ -17,11 +18,11 @@ class CrazyJsonString(String):
|
||||
|
||||
class TestFields(object):
|
||||
# Will be returned by editable_metadata_fields.
|
||||
max_attempts = StringyInteger(scope=Scope.settings, default=1000, values={'min': 1, 'max': 10})
|
||||
max_attempts = Integer(scope=Scope.settings, default=1000, values={'min': 1, 'max': 10})
|
||||
# Will not be returned by editable_metadata_fields because filtered out by non_editable_metadata_fields.
|
||||
due = Date(scope=Scope.settings)
|
||||
# Will not be returned by editable_metadata_fields because is not Scope.settings.
|
||||
student_answers = Object(scope=Scope.user_state)
|
||||
student_answers = Dict(scope=Scope.user_state)
|
||||
# Will be returned, and can override the inherited value from XModule.
|
||||
display_name = String(scope=Scope.settings, default='local default', display_name='Local Display Name',
|
||||
help='local help')
|
||||
@@ -33,9 +34,9 @@ class TestFields(object):
|
||||
{'display_name': 'second', 'value': 'value b'}]
|
||||
)
|
||||
# Used for testing select type
|
||||
float_select = StringyFloat(scope=Scope.settings, default=.999, values=[1.23, 0.98])
|
||||
float_select = Float(scope=Scope.settings, default=.999, values=[1.23, 0.98])
|
||||
# Used for testing float type
|
||||
float_non_select = StringyFloat(scope=Scope.settings, default=.999, values={'min': 0, 'step': .3})
|
||||
float_non_select = Float(scope=Scope.settings, default=.999, values={'min': 0, 'step': .3})
|
||||
# Used for testing that Booleans get mapped to select type
|
||||
boolean_select = Boolean(scope=Scope.settings)
|
||||
|
||||
@@ -104,7 +105,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
|
||||
|
||||
def test_type_and_options(self):
|
||||
# test_display_name_field verifies that a String field is of type "Generic".
|
||||
# test_integer_field verifies that a StringyInteger field is of type "Integer".
|
||||
# test_integer_field verifies that a Integer field is of type "Integer".
|
||||
|
||||
descriptor = self.get_descriptor({})
|
||||
editable_fields = descriptor.editable_metadata_fields
|
||||
@@ -171,3 +172,194 @@ class EditableMetadataFieldsTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(explicitly_set, test_field['explicitly_set'])
|
||||
self.assertEqual(inheritable, test_field['inheritable'])
|
||||
|
||||
|
||||
class TestSerialize(unittest.TestCase):
|
||||
""" Tests the serialize, method, which is not dependent on type. """
|
||||
def test_serialize(self):
|
||||
assert_equals('null', serialize_field(None))
|
||||
assert_equals('-2', serialize_field(-2))
|
||||
assert_equals('"2"', serialize_field('2'))
|
||||
assert_equals('-3.41', serialize_field(-3.41))
|
||||
assert_equals('"2.589"', serialize_field('2.589'))
|
||||
assert_equals('false', serialize_field(False))
|
||||
assert_equals('"false"', serialize_field('false'))
|
||||
assert_equals('"fAlse"', serialize_field('fAlse'))
|
||||
assert_equals('"hat box"', serialize_field('hat box'))
|
||||
assert_equals('{"bar": "hat", "frog": "green"}', serialize_field({'bar': 'hat', 'frog' : 'green'}))
|
||||
assert_equals('[3.5, 5.6]', serialize_field([3.5, 5.6]))
|
||||
assert_equals('["foo", "bar"]', serialize_field(['foo', 'bar']))
|
||||
assert_equals('"2012-12-31T23:59:59Z"', serialize_field("2012-12-31T23:59:59Z"))
|
||||
assert_equals('"1 day 12 hours 59 minutes 59 seconds"',
|
||||
serialize_field("1 day 12 hours 59 minutes 59 seconds"))
|
||||
|
||||
|
||||
class TestDeserialize(unittest.TestCase):
|
||||
def assertDeserializeEqual(self, expected, arg):
|
||||
"""
|
||||
Asserts the result of deserialize_field.
|
||||
"""
|
||||
assert_equals(expected, deserialize_field(self.test_field(), arg))
|
||||
|
||||
|
||||
def assertDeserializeNonString(self):
|
||||
"""
|
||||
Asserts input value is returned for None or something that is not a string.
|
||||
For all types, 'null' is also always returned as None.
|
||||
"""
|
||||
self.assertDeserializeEqual(None, None)
|
||||
self.assertDeserializeEqual(3.14, 3.14)
|
||||
self.assertDeserializeEqual(True, True)
|
||||
self.assertDeserializeEqual([10], [10])
|
||||
self.assertDeserializeEqual({}, {})
|
||||
self.assertDeserializeEqual([], [])
|
||||
self.assertDeserializeEqual(None, 'null')
|
||||
|
||||
|
||||
class TestDeserializeInteger(TestDeserialize):
|
||||
""" Tests deserialize as related to Integer type. """
|
||||
|
||||
test_field = Integer
|
||||
|
||||
def test_deserialize(self):
|
||||
self.assertDeserializeEqual(-2, '-2')
|
||||
self.assertDeserializeEqual("450", '"450"')
|
||||
|
||||
# False can be parsed as a int (converts to 0)
|
||||
self.assertDeserializeEqual(False, 'false')
|
||||
# True can be parsed as a int (converts to 1)
|
||||
self.assertDeserializeEqual(True, 'true')
|
||||
# 2.78 can be converted to int, so the string will be deserialized
|
||||
self.assertDeserializeEqual(-2.78, '-2.78')
|
||||
|
||||
|
||||
def test_deserialize_unsupported_types(self):
|
||||
self.assertDeserializeEqual('[3]', '[3]')
|
||||
# '2.78' cannot be converted to int, so input value is returned
|
||||
self.assertDeserializeEqual('"-2.78"', '"-2.78"')
|
||||
# 'false' cannot be converted to int, so input value is returned
|
||||
self.assertDeserializeEqual('"false"', '"false"')
|
||||
self.assertDeserializeNonString()
|
||||
|
||||
|
||||
class TestDeserializeFloat(TestDeserialize):
|
||||
""" Tests deserialize as related to Float type. """
|
||||
|
||||
test_field = Float
|
||||
|
||||
def test_deserialize(self):
|
||||
self.assertDeserializeEqual( -2, '-2')
|
||||
self.assertDeserializeEqual("450", '"450"')
|
||||
self.assertDeserializeEqual(-2.78, '-2.78')
|
||||
self.assertDeserializeEqual("0.45", '"0.45"')
|
||||
|
||||
# False can be parsed as a float (converts to 0)
|
||||
self.assertDeserializeEqual(False, 'false')
|
||||
# True can be parsed as a float (converts to 1)
|
||||
self.assertDeserializeEqual( True, 'true')
|
||||
|
||||
def test_deserialize_unsupported_types(self):
|
||||
self.assertDeserializeEqual('[3]', '[3]')
|
||||
# 'false' cannot be converted to float, so input value is returned
|
||||
self.assertDeserializeEqual('"false"', '"false"')
|
||||
self.assertDeserializeNonString()
|
||||
|
||||
|
||||
class TestDeserializeBoolean(TestDeserialize):
|
||||
""" Tests deserialize as related to Boolean type. """
|
||||
|
||||
test_field = Boolean
|
||||
|
||||
def test_deserialize(self):
|
||||
# json.loads converts the value to Python bool
|
||||
self.assertDeserializeEqual(False, 'false')
|
||||
self.assertDeserializeEqual(True, 'true')
|
||||
|
||||
# json.loads fails, string value is returned.
|
||||
self.assertDeserializeEqual('False', 'False')
|
||||
self.assertDeserializeEqual('True', 'True')
|
||||
|
||||
# json.loads deserializes as a string
|
||||
self.assertDeserializeEqual('false', '"false"')
|
||||
self.assertDeserializeEqual('fAlse', '"fAlse"')
|
||||
self.assertDeserializeEqual("TruE", '"TruE"')
|
||||
|
||||
# 2.78 can be converted to a bool, so the string will be deserialized
|
||||
self.assertDeserializeEqual(-2.78, '-2.78')
|
||||
|
||||
self.assertDeserializeNonString()
|
||||
|
||||
|
||||
class TestDeserializeString(TestDeserialize):
|
||||
""" Tests deserialize as related to String type. """
|
||||
|
||||
test_field = String
|
||||
|
||||
def test_deserialize(self):
|
||||
self.assertDeserializeEqual('hAlf', '"hAlf"')
|
||||
self.assertDeserializeEqual('false', '"false"')
|
||||
self.assertDeserializeEqual('single quote', 'single quote')
|
||||
|
||||
def test_deserialize_unsupported_types(self):
|
||||
self.assertDeserializeEqual('3.4', '3.4')
|
||||
self.assertDeserializeEqual('false', 'false')
|
||||
self.assertDeserializeEqual('2', '2')
|
||||
self.assertDeserializeEqual('[3]', '[3]')
|
||||
self.assertDeserializeNonString()
|
||||
|
||||
|
||||
class TestDeserializeAny(TestDeserialize):
|
||||
""" Tests deserialize as related to Any type. """
|
||||
|
||||
test_field = Any
|
||||
|
||||
def test_deserialize(self):
|
||||
self.assertDeserializeEqual('hAlf', '"hAlf"')
|
||||
self.assertDeserializeEqual('false', '"false"')
|
||||
self.assertDeserializeEqual({'bar': 'hat', 'frog' : 'green'}, '{"bar": "hat", "frog": "green"}')
|
||||
self.assertDeserializeEqual([3.5, 5.6], '[3.5, 5.6]')
|
||||
self.assertDeserializeEqual('[', '[')
|
||||
self.assertDeserializeEqual(False, 'false')
|
||||
self.assertDeserializeEqual(3.4, '3.4')
|
||||
self.assertDeserializeNonString()
|
||||
|
||||
|
||||
class TestDeserializeList(TestDeserialize):
|
||||
""" Tests deserialize as related to List type. """
|
||||
|
||||
test_field = List
|
||||
|
||||
def test_deserialize(self):
|
||||
self.assertDeserializeEqual(['foo', 'bar'], '["foo", "bar"]')
|
||||
self.assertDeserializeEqual([3.5, 5.6], '[3.5, 5.6]')
|
||||
self.assertDeserializeEqual([], '[]')
|
||||
|
||||
def test_deserialize_unsupported_types(self):
|
||||
self.assertDeserializeEqual('3.4', '3.4')
|
||||
self.assertDeserializeEqual('false', 'false')
|
||||
self.assertDeserializeEqual('2', '2')
|
||||
self.assertDeserializeNonString()
|
||||
|
||||
|
||||
class TestDeserializeDate(TestDeserialize):
|
||||
""" Tests deserialize as related to Date type. """
|
||||
|
||||
test_field = Date
|
||||
|
||||
def test_deserialize(self):
|
||||
self.assertDeserializeEqual('2012-12-31T23:59:59Z', "2012-12-31T23:59:59Z")
|
||||
self.assertDeserializeEqual('2012-12-31T23:59:59Z', '"2012-12-31T23:59:59Z"')
|
||||
self.assertDeserializeNonString()
|
||||
|
||||
|
||||
class TestDeserializeTimedelta(TestDeserialize):
|
||||
""" Tests deserialize as related to Timedelta type. """
|
||||
|
||||
test_field = Timedelta
|
||||
|
||||
def test_deserialize(self):
|
||||
self.assertDeserializeEqual('1 day 12 hours 59 minutes 59 seconds',
|
||||
'1 day 12 hours 59 minutes 59 seconds')
|
||||
self.assertDeserializeEqual('1 day 12 hours 59 minutes 59 seconds',
|
||||
'"1 day 12 hours 59 minutes 59 seconds"')
|
||||
self.assertDeserializeNonString()
|
||||
|
||||
@@ -123,9 +123,6 @@ class TimeLimitDescriptor(TimeLimitFields, XMLEditingDescriptor, XmlDescriptor):
|
||||
|
||||
module_class = TimeLimitModule
|
||||
|
||||
# For remembering when a student started, and when they should end
|
||||
stores_state = True
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
children = []
|
||||
|
||||
@@ -86,7 +86,6 @@ class VideoDescriptor(VideoFields,
|
||||
MetadataOnlyEditingDescriptor,
|
||||
RawDescriptor):
|
||||
module_class = VideoModule
|
||||
stores_state = True
|
||||
template_dir_name = "video"
|
||||
|
||||
@property
|
||||
|
||||
@@ -5,6 +5,7 @@ from lxml import etree
|
||||
from pkg_resources import resource_string, resource_listdir
|
||||
|
||||
from django.http import Http404
|
||||
from django.conf import settings
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
@@ -147,11 +148,11 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
|
||||
'caption_asset_path': caption_asset_path,
|
||||
'show_captions': self.show_captions,
|
||||
'start': self.start_time,
|
||||
'end': self.end_time
|
||||
'end': self.end_time,
|
||||
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
|
||||
})
|
||||
|
||||
|
||||
class VideoAlphaDescriptor(VideoAlphaFields, RawDescriptor):
|
||||
module_class = VideoAlphaModule
|
||||
stores_state = True
|
||||
template_dir_name = "videoalpha"
|
||||
|
||||
@@ -14,8 +14,7 @@ from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.editing_module import MetadataOnlyEditingDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
|
||||
from xblock.core import Scope, Object, Boolean, List
|
||||
from fields import StringyBoolean, StringyInteger
|
||||
from xblock.core import Scope, Dict, Boolean, List, Integer
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,21 +31,21 @@ def pretty_bool(value):
|
||||
|
||||
class WordCloudFields(object):
|
||||
"""XFields for word cloud."""
|
||||
num_inputs = StringyInteger(
|
||||
num_inputs = Integer(
|
||||
display_name="Inputs",
|
||||
help="Number of text boxes available for students to input words/sentences.",
|
||||
scope=Scope.settings,
|
||||
default=5,
|
||||
values={"min": 1}
|
||||
)
|
||||
num_top_words = StringyInteger(
|
||||
num_top_words = Integer(
|
||||
display_name="Maximum Words",
|
||||
help="Maximum number of words to be displayed in generated word cloud.",
|
||||
scope=Scope.settings,
|
||||
default=250,
|
||||
values={"min": 1}
|
||||
)
|
||||
display_student_percents = StringyBoolean(
|
||||
display_student_percents = Boolean(
|
||||
display_name="Show Percents",
|
||||
help="Statistics are shown for entered words near that word.",
|
||||
scope=Scope.settings,
|
||||
@@ -64,11 +63,11 @@ class WordCloudFields(object):
|
||||
scope=Scope.user_state,
|
||||
default=[]
|
||||
)
|
||||
all_words = Object(
|
||||
all_words = Dict(
|
||||
help="All possible words from all students.",
|
||||
scope=Scope.content
|
||||
)
|
||||
top_words = Object(
|
||||
top_words = Dict(
|
||||
help="Top num_top_words words for word cloud.",
|
||||
scope=Scope.content
|
||||
)
|
||||
@@ -239,4 +238,3 @@ class WordCloudDescriptor(MetadataOnlyEditingDescriptor, RawDescriptor, WordClou
|
||||
"""Descriptor for WordCloud Xmodule."""
|
||||
module_class = WordCloudModule
|
||||
template_dir_name = 'word_cloud'
|
||||
stores_state = True
|
||||
|
||||
@@ -327,10 +327,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
|
||||
# Attributes for inspection of the descriptor
|
||||
|
||||
# Indicates whether the xmodule state should be
|
||||
# stored in a database (independent of shared state)
|
||||
stores_state = False
|
||||
|
||||
# This indicates whether the xmodule is a problem-type.
|
||||
# It should respond to max_score() and grade(). It can be graded or ungraded
|
||||
# (like a practice problem).
|
||||
|
||||
@@ -6,7 +6,7 @@ import sys
|
||||
from collections import namedtuple
|
||||
from lxml import etree
|
||||
|
||||
from xblock.core import Object, Scope
|
||||
from xblock.core import Dict, Scope
|
||||
from xmodule.x_module import (XModuleDescriptor, policy_key)
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
@@ -79,12 +79,53 @@ class AttrMap(_AttrMapBase):
|
||||
return _AttrMapBase.__new__(_cls, from_xml, to_xml)
|
||||
|
||||
|
||||
def serialize_field(value):
|
||||
"""
|
||||
Return a string version of the value (where value is the JSON-formatted, internally stored value).
|
||||
|
||||
By default, this is the result of calling json.dumps on the input value.
|
||||
"""
|
||||
return json.dumps(value)
|
||||
|
||||
|
||||
def deserialize_field(field, value):
|
||||
"""
|
||||
Deserialize the string version to the value stored internally.
|
||||
|
||||
Note that this is not the same as the value returned by from_json, as model types typically store
|
||||
their value internally as JSON. By default, this method will return the result of calling json.loads
|
||||
on the supplied value, unless json.loads throws a TypeError, or the type of the value returned by json.loads
|
||||
is not supported for this class (from_json throws an Error). In either of those cases, this method returns
|
||||
the input value.
|
||||
"""
|
||||
try:
|
||||
deserialized = json.loads(value)
|
||||
if deserialized is None:
|
||||
return deserialized
|
||||
try:
|
||||
field.from_json(deserialized)
|
||||
return deserialized
|
||||
except (ValueError, TypeError):
|
||||
# Support older serialized version, which was just a string, not result of json.dumps.
|
||||
# If the deserialized version cannot be converted to the type (via from_json),
|
||||
# just return the original value. For example, if a string value of '3.4' was
|
||||
# stored for a String field (before we started storing the result of json.dumps),
|
||||
# then it would be deserialized as 3.4, but 3.4 is not supported for a String
|
||||
# field. Therefore field.from_json(3.4) will throw an Error, and we should
|
||||
# actually return the original value of '3.4'.
|
||||
return value
|
||||
|
||||
except (ValueError, TypeError):
|
||||
# Support older serialized version.
|
||||
return value
|
||||
|
||||
|
||||
class XmlDescriptor(XModuleDescriptor):
|
||||
"""
|
||||
Mixin class for standardized parsing of from xml
|
||||
"""
|
||||
|
||||
xml_attributes = Object(help="Map of unhandled xml attributes, used only for storage between import and export",
|
||||
xml_attributes = Dict(help="Map of unhandled xml attributes, used only for storage between import and export",
|
||||
default={}, scope=Scope.settings)
|
||||
|
||||
# Extension to append to filename paths
|
||||
@@ -120,25 +161,15 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
|
||||
metadata_to_export_to_policy = ('discussion_topics')
|
||||
|
||||
# A dictionary mapping xml attribute names AttrMaps that describe how
|
||||
# to import and export them
|
||||
# Allow json to specify either the string "true", or the bool True. The string is preferred.
|
||||
to_bool = lambda val: val == 'true' or val == True
|
||||
from_bool = lambda val: str(val).lower()
|
||||
bool_map = AttrMap(to_bool, from_bool)
|
||||
|
||||
to_int = lambda val: int(val)
|
||||
from_int = lambda val: str(val)
|
||||
int_map = AttrMap(to_int, from_int)
|
||||
xml_attribute_map = {
|
||||
# type conversion: want True/False in python, "true"/"false" in xml
|
||||
'graded': bool_map,
|
||||
'hide_progress_tab': bool_map,
|
||||
'allow_anonymous': bool_map,
|
||||
'allow_anonymous_to_peers': bool_map,
|
||||
'show_timezone': bool_map,
|
||||
}
|
||||
@classmethod
|
||||
def get_map_for_field(cls, attr):
|
||||
for field in set(cls.fields + cls.lms.fields):
|
||||
if field.name == attr:
|
||||
from_xml = lambda val: deserialize_field(field, val)
|
||||
to_xml = lambda val : serialize_field(val)
|
||||
return AttrMap(from_xml, to_xml)
|
||||
|
||||
return AttrMap()
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
@@ -188,7 +219,6 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
filepath, location.url(), str(err))
|
||||
raise Exception, msg, sys.exc_info()[2]
|
||||
|
||||
|
||||
@classmethod
|
||||
def load_definition(cls, xml_object, system, location):
|
||||
'''Load a descriptor definition from the specified xml_object.
|
||||
@@ -246,7 +276,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
# don't load these
|
||||
continue
|
||||
|
||||
attr_map = cls.xml_attribute_map.get(attr, AttrMap())
|
||||
attr_map = cls.get_map_for_field(attr)
|
||||
metadata[attr] = attr_map.from_xml(val)
|
||||
return metadata
|
||||
|
||||
@@ -258,7 +288,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
through the attrmap. Updates the metadata dict in place.
|
||||
"""
|
||||
for attr in policy:
|
||||
attr_map = cls.xml_attribute_map.get(attr, AttrMap())
|
||||
attr_map = cls.get_map_for_field(attr)
|
||||
metadata[cls._translate(attr)] = attr_map.from_xml(policy[attr])
|
||||
|
||||
@classmethod
|
||||
@@ -347,7 +377,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
|
||||
def export_to_xml(self, resource_fs):
|
||||
"""
|
||||
Returns an xml string representign this module, and all modules
|
||||
Returns an xml string representing this module, and all modules
|
||||
underneath it. May also write required resources out to resource_fs
|
||||
|
||||
Assumes that modules have single parentage (that no module appears twice
|
||||
@@ -372,7 +402,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
"""Get the value for this attribute that we want to store.
|
||||
(Possible format conversion through an AttrMap).
|
||||
"""
|
||||
attr_map = self.xml_attribute_map.get(attr, AttrMap())
|
||||
attr_map = self.get_map_for_field(attr)
|
||||
return attr_map.to_xml(self._model_data[attr])
|
||||
|
||||
# Add the non-inherited metadata
|
||||
|
||||
@@ -1 +1 @@
|
||||
<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="["videoalpha"]"/>
|
||||
|
||||
6
lms/djangoapps/courseware/features/videoalpha.feature
Normal file
6
lms/djangoapps/courseware/features/videoalpha.feature
Normal file
@@ -0,0 +1,6 @@
|
||||
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
|
||||
36
lms/djangoapps/courseware/features/videoalpha.py
Normal file
36
lms/djangoapps/courseware/features/videoalpha.py
Normal file
@@ -0,0 +1,36 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0613
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from lettuce.django import django_url
|
||||
from common import TEST_COURSE_NAME, TEST_SECTION_NAME, i_am_registered_for_the_course, section_location
|
||||
|
||||
############### ACTIONS ####################
|
||||
|
||||
|
||||
@step('when I view the video it has autoplay enabled')
|
||||
def does_autoplay(step):
|
||||
assert(world.css_find('.videoalpha')[0]['data-autoplay'] == 'True')
|
||||
|
||||
|
||||
@step('the course has a Video component')
|
||||
def view_videoalpha(step):
|
||||
coursename = TEST_COURSE_NAME.replace(' ', '_')
|
||||
i_am_registered_for_the_course(step, coursename)
|
||||
|
||||
# Make sure we have a videoalpha
|
||||
add_videoalpha_to_course(coursename)
|
||||
chapter_name = TEST_SECTION_NAME.replace(" ", "_")
|
||||
section_name = chapter_name
|
||||
url = django_url('/courses/edx/Test_Course/Test_Course/courseware/%s/%s' %
|
||||
(chapter_name, section_name))
|
||||
|
||||
world.browser.visit(url)
|
||||
|
||||
|
||||
def add_videoalpha_to_course(course):
|
||||
template_name = 'i4x://edx/templates/videoalpha/default'
|
||||
world.ItemFactory.create(parent_location=section_location(course),
|
||||
template=template_name,
|
||||
display_name='Video Alpha 1')
|
||||
@@ -364,7 +364,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
if not (problem_descriptor.stores_state and problem_descriptor.has_score):
|
||||
if not problem_descriptor.has_score:
|
||||
# These are not problems, and do not have a score
|
||||
return (None, None)
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ def mock_field(scope, name):
|
||||
|
||||
def mock_descriptor(fields=[], lms_fields=[]):
|
||||
descriptor = Mock()
|
||||
descriptor.stores_state = True
|
||||
descriptor.location = location('def_id')
|
||||
descriptor.module_class.fields = fields
|
||||
descriptor.module_class.lms.fields = lms_fields
|
||||
|
||||
@@ -13,6 +13,7 @@ from xmodule.modulestore.django import modulestore
|
||||
|
||||
import courseware.views as views
|
||||
from xmodule.modulestore import Location
|
||||
from pytz import UTC
|
||||
|
||||
|
||||
class Stub():
|
||||
@@ -63,7 +64,7 @@ class ViewsTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(username='dummy', password='123456',
|
||||
email='test@mit.edu')
|
||||
self.date = datetime.datetime(2013, 1, 22)
|
||||
self.date = datetime.datetime(2013, 1, 22, tzinfo=UTC)
|
||||
self.course_id = 'edX/toy/2012_Fall'
|
||||
self.enrollment = CourseEnrollment.objects.get_or_create(user=self.user,
|
||||
course_id=self.course_id,
|
||||
|
||||
@@ -172,8 +172,9 @@ for name, value in ENV_TOKENS.get("CODE_JAIL", {}).items():
|
||||
|
||||
COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", [])
|
||||
|
||||
############### Mixed Related(Secure/Not-Secure) Items ##########
|
||||
# If segment.io key specified, load it and turn on segment IO if the feature flag is set
|
||||
SEGMENT_IO_LMS_KEY = ENV_TOKENS.get('SEGMENT_IO_LMS_KEY')
|
||||
SEGMENT_IO_LMS_KEY = AUTH_TOKENS.get('SEGMENT_IO_LMS_KEY')
|
||||
if SEGMENT_IO_LMS_KEY:
|
||||
MITX_FEATURES['SEGMENT_IO_LMS'] = ENV_TOKENS.get('SEGMENT_IO_LMS', False)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
data-start="${start}"
|
||||
data-end="${end}"
|
||||
data-caption-asset-path="${caption_asset_path}"
|
||||
data-autoplay="${autoplay}"
|
||||
>
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
% if settings.MITX_FEATURES.get('SEGMENT_IO_LMS'):
|
||||
<!-- begin Segment.io -->
|
||||
<script type="text/javascript">
|
||||
// Leaving this line out of the feature flag block is intentional. Pulling the line outside of the if statement allows it to serve as its own dummy object.
|
||||
var analytics=analytics||[];analytics.load=function(e){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=("https:"===document.location.protocol?"https://":"http://")+"d2dq2ahtl5zl1z.cloudfront.net/analytics.js/v1/"+e+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n);var r=function(e){return function(){analytics.push([e].concat(Array.prototype.slice.call(arguments,0)))}},i=["identify","track","trackLink","trackForm","trackClick","trackSubmit","pageview","ab","alias","ready"];for(var s=0;s<i.length;s++)analytics[i[s]]=r(i[s])};
|
||||
|
||||
% if settings.MITX_FEATURES.get('SEGMENT_IO_LMS'):
|
||||
analytics.load("${ settings.SEGMENT_IO_LMS_KEY }");
|
||||
|
||||
% if user.is_authenticated():
|
||||
@@ -11,14 +13,6 @@
|
||||
});
|
||||
|
||||
% endif
|
||||
% endif
|
||||
</script>
|
||||
<!-- end Segment.io -->
|
||||
% else:
|
||||
<!-- dummy segment.io -->
|
||||
<script type="text/javascript">
|
||||
var analytics = {
|
||||
track: function() { return; }
|
||||
};
|
||||
</script>
|
||||
<!-- end dummy segment.io -->
|
||||
% endif
|
||||
|
||||
@@ -98,6 +98,8 @@ if not settings.MITX_FEATURES["USE_CUSTOM_THEME"]:
|
||||
url(r'^press$', 'student.views.press', name="press"),
|
||||
url(r'^media-kit$', 'static_template_view.views.render',
|
||||
{'template': 'media-kit.html'}, name="media-kit"),
|
||||
url(r'^faq$', 'static_template_view.views.render',
|
||||
{'template': 'faq.html'}, name="faq_edx"),
|
||||
url(r'^help$', 'static_template_view.views.render',
|
||||
{'template': 'help.html'}, name="help_edx"),
|
||||
|
||||
@@ -125,7 +127,7 @@ for key, value in settings.MKTG_URL_LINK_MAP.items():
|
||||
continue
|
||||
|
||||
# These urls are enabled separately
|
||||
if key == "ROOT" or key == "COURSES":
|
||||
if key == "ROOT" or key == "COURSES" or key == "FAQ":
|
||||
continue
|
||||
|
||||
# Make the assumptions that the templates are all in the same dir
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"""
|
||||
Namespace that defines fields common to all blocks used in the LMS
|
||||
"""
|
||||
from xblock.core import Namespace, Boolean, Scope, String
|
||||
from xmodule.fields import Date, Timedelta, StringyFloat, StringyBoolean
|
||||
from xblock.core import Namespace, Boolean, Scope, String, Float
|
||||
from xmodule.fields import Date, Timedelta
|
||||
|
||||
|
||||
class LmsNamespace(Namespace):
|
||||
"""
|
||||
Namespace that defines fields common to all blocks used in the LMS
|
||||
"""
|
||||
hide_from_toc = StringyBoolean(
|
||||
hide_from_toc = Boolean(
|
||||
help="Whether to display this module in the table of contents",
|
||||
default=False,
|
||||
scope=Scope.settings
|
||||
@@ -37,7 +37,7 @@ class LmsNamespace(Namespace):
|
||||
)
|
||||
showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
|
||||
rerandomize = String(help="When to rerandomize the problem", default="always", scope=Scope.settings)
|
||||
days_early_for_beta = StringyFloat(
|
||||
days_early_for_beta = Float(
|
||||
help="Number of days early to show content to beta users",
|
||||
default=None,
|
||||
scope=Scope.settings
|
||||
|
||||
@@ -52,7 +52,7 @@ def sass_cmd(watch=false, debug=false)
|
||||
"sass #{debug ? '--debug-info' : '--style compressed'} " +
|
||||
"--load-path #{sass_load_paths.join(' ')} " +
|
||||
"--require ./common/static/sass/bourbon/lib/bourbon.rb " +
|
||||
"#{watch ? '--watch' : '--update'} #{sass_watch_paths.join(' ')}"
|
||||
"#{watch ? '--watch' : '--update'} -E utf-8 #{sass_watch_paths.join(' ')}"
|
||||
end
|
||||
|
||||
desc "Compile all assets"
|
||||
@@ -78,7 +78,7 @@ namespace :assets do
|
||||
end
|
||||
|
||||
{:xmodule => [:install_python_prereqs],
|
||||
:coffee => [:install_node_prereqs],
|
||||
:coffee => [:install_node_prereqs, :'assets:coffee:clobber'],
|
||||
:sass => [:install_ruby_prereqs, :preprocess]}.each_pair do |asset_type, prereq_tasks|
|
||||
desc "Compile all #{asset_type} assets"
|
||||
task asset_type => prereq_tasks do
|
||||
@@ -127,6 +127,11 @@ namespace :assets do
|
||||
multitask :coffee => 'assets:xmodule'
|
||||
namespace :coffee do
|
||||
multitask :debug => 'assets:xmodule:debug'
|
||||
|
||||
desc "Remove compiled coffeescript files"
|
||||
task :clobber do
|
||||
FileList['*/static/coffee/**/*.js'].each {|f| File.delete(f)}
|
||||
end
|
||||
end
|
||||
|
||||
namespace :xmodule do
|
||||
|
||||
@@ -61,10 +61,10 @@ def template_jasmine_runner(lib)
|
||||
yield File.expand_path(template_output)
|
||||
end
|
||||
|
||||
def jasmine_browser(url, wait=10)
|
||||
def jasmine_browser(url, jitter=3, wait=10)
|
||||
# Jitter starting the browser so that the tests don't all try and
|
||||
# start the browser simultaneously
|
||||
sleep(rand(3))
|
||||
sleep(rand(jitter))
|
||||
sh("python -m webbrowser -t '#{url}'")
|
||||
sleep(wait)
|
||||
end
|
||||
@@ -87,6 +87,15 @@ end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Open jasmine tests for #{system} in your default browser, and dynamically recompile coffeescript"
|
||||
task :'browser:watch' => :'assets:coffee:_watch' do
|
||||
django_for_jasmine(system, true) do |jasmine_url|
|
||||
jasmine_browser(jasmine_url, jitter=0, wait=0)
|
||||
end
|
||||
puts "Press ENTER to terminate".red
|
||||
$stdin.gets
|
||||
end
|
||||
|
||||
desc "Use phantomjs to run jasmine tests for #{system} from the console"
|
||||
task :phantomjs do
|
||||
Rake::Task[:assets].invoke(system, 'jasmine')
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
-e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
|
||||
|
||||
# Our libraries:
|
||||
-e git+https://github.com/edx/XBlock.git@2144a25d#egg=XBlock
|
||||
-e git+https://github.com/edx/XBlock.git@4d8735e883#egg=XBlock
|
||||
-e git+https://github.com/edx/codejail.git@0a1b468#egg=codejail
|
||||
-e git+https://github.com/edx/diff-cover.git@v0.1.1#egg=diff_cover
|
||||
|
||||
Reference in New Issue
Block a user