diff --git a/.pylintrc b/.pylintrc
index 6690bb7df0..9ea1e62ad4 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -41,7 +41,8 @@ disable=
# R0902: Too many instance attributes
# R0903: Too few public methods (1/2)
# R0904: Too many public methods
- W0141,W0142,R0201,R0901,R0902,R0903,R0904
+# R0913: Too many arguments
+ W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913
[REPORTS]
@@ -137,7 +138,7 @@ bad-names=foo,bar,baz,toto,tutu,tata
# Regular expression which should only match functions or classes name which do
# not require a docstring
-no-docstring-rgx=__.*__
+no-docstring-rgx=(__.*__|test_.*)
[MISCELLANEOUS]
diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py
index 8c8aed549d..589db4ac56 100644
--- a/cms/djangoapps/contentstore/course_info_model.py
+++ b/cms/djangoapps/contentstore/course_info_model.py
@@ -1,13 +1,15 @@
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
-from lxml import html, etree
+from lxml import html
import re
from django.http import HttpResponseBadRequest
import logging
+import django.utils
-## TODO store as array of { date, content } and override course_info_module.definition_from_xml
-## This should be in a class which inherits from XmlDescriptor
+# # TODO store as array of { date, content } and override course_info_module.definition_from_xml
+# # This should be in a class which inherits from XmlDescriptor
+log = logging.getLogger(__name__)
def get_course_updates(location):
@@ -26,9 +28,11 @@ def get_course_updates(location):
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
- course_html_parsed = etree.fromstring(course_updates.data)
- except etree.XMLSyntaxError:
- course_html_parsed = etree.fromstring("
diff --git a/cms/urls.py b/cms/urls.py
index 69ce4a540d..e1eae3352a 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -42,36 +42,52 @@ urlpatterns = ('',
'contentstore.views.remove_user', name='remove_user'),
url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)/remove_user$',
'contentstore.views.remove_user', name='remove_user'),
- url(r'^(?P[^/]+)/(?P[^/]+)/info/(?P[^/]+)$', 'contentstore.views.course_info', name='course_info'),
- url(r'^(?P[^/]+)/(?P[^/]+)/course_info/updates/(?P.*)$', 'contentstore.views.course_info_updates', name='course_info'),
- url(r'^(?P[^/]+)/(?P[^/]+)/settings-details/(?P[^/]+)$', 'contentstore.views.get_course_settings', name='course_settings'),
- url(r'^(?P[^/]+)/(?P[^/]+)/settings-grading/(?P[^/]+)$', 'contentstore.views.course_config_graders_page', name='course_settings'),
- url(r'^(?P[^/]+)/(?P[^/]+)/settings-details/(?P[^/]+)/section/(?P[^/]+).*$', 'contentstore.views.course_settings_updates', name='course_settings'),
- url(r'^(?P[^/]+)/(?P[^/]+)/settings-grading/(?P[^/]+)/(?P.*)$', 'contentstore.views.course_grader_updates', name='course_settings'),
+ url(r'^(?P[^/]+)/(?P[^/]+)/info/(?P[^/]+)$',
+ 'contentstore.views.course_info', name='course_info'),
+ url(r'^(?P[^/]+)/(?P[^/]+)/course_info/updates/(?P.*)$',
+ 'contentstore.views.course_info_updates', name='course_info_json'),
+ url(r'^(?P[^/]+)/(?P[^/]+)/settings-details/(?P[^/]+)$',
+ 'contentstore.views.get_course_settings', name='settings_details'),
+ url(r'^(?P[^/]+)/(?P[^/]+)/settings-grading/(?P[^/]+)$',
+ 'contentstore.views.course_config_graders_page', name='settings_grading'),
+ url(r'^(?P[^/]+)/(?P[^/]+)/settings-details/(?P[^/]+)/section/(?P[^/]+).*$',
+ 'contentstore.views.course_settings_updates', name='course_settings'),
+ url(r'^(?P[^/]+)/(?P[^/]+)/settings-grading/(?P[^/]+)/(?P.*)$',
+ 'contentstore.views.course_grader_updates', name='course_settings'),
# This is the URL to initially render the course advanced settings.
- url(r'^(?P[^/]+)/(?P[^/]+)/settings-advanced/(?P[^/]+)$', 'contentstore.views.course_config_advanced_page', name='course_advanced_settings'),
+ url(r'^(?P[^/]+)/(?P[^/]+)/settings-advanced/(?P[^/]+)$',
+ 'contentstore.views.course_config_advanced_page', name='course_advanced_settings'),
# This is the URL used by BackBone for updating and re-fetching the model.
- url(r'^(?P[^/]+)/(?P[^/]+)/settings-advanced/(?P[^/]+)/update.*$', 'contentstore.views.course_advanced_updates', name='course_advanced_settings_updates'),
+ url(r'^(?P[^/]+)/(?P[^/]+)/settings-advanced/(?P[^/]+)/update.*$',
+ 'contentstore.views.course_advanced_updates', name='course_advanced_settings_updates'),
- url(r'^(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/gradeas.*$', 'contentstore.views.assignment_type_update', name='assignment_type_update'),
+ url(r'^(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/gradeas.*$',
+ 'contentstore.views.assignment_type_update', name='assignment_type_update'),
- url(r'^pages/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.static_pages',
+ url(r'^pages/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$',
+ 'contentstore.views.static_pages',
name='static_pages'),
- url(r'^edit_static/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.edit_static', name='edit_static'),
- url(r'^edit_tabs/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.edit_tabs', name='edit_tabs'),
- url(r'^(?P[^/]+)/(?P[^/]+)/assets/(?P[^/]+)$', 'contentstore.views.asset_index', name='asset_index'),
+ url(r'^edit_static/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$',
+ 'contentstore.views.edit_static', name='edit_static'),
+ url(r'^edit_tabs/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$',
+ 'contentstore.views.edit_tabs', name='edit_tabs'),
+ url(r'^(?P[^/]+)/(?P[^/]+)/assets/(?P[^/]+)$',
+ 'contentstore.views.asset_index', name='asset_index'),
# this is a generic method to return the data/metadata associated with a xmodule
- url(r'^module_info/(?P.*)$', 'contentstore.views.module_info', name='module_info'),
+ url(r'^module_info/(?P.*)$',
+ 'contentstore.views.module_info', name='module_info'),
# temporary landing page for a course
- url(r'^edge/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.landing', name='landing'),
+ url(r'^edge/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$',
+ 'contentstore.views.landing', name='landing'),
url(r'^not_found$', 'contentstore.views.not_found', name='not_found'),
url(r'^server_error$', 'contentstore.views.server_error', name='server_error'),
- url(r'^(?P[^/]+)/(?P[^/]+)/assets/(?P[^/]+)$', 'contentstore.views.asset_index', name='asset_index'),
+ url(r'^(?P[^/]+)/(?P[^/]+)/assets/(?P[^/]+)$',
+ 'contentstore.views.asset_index', name='asset_index'),
# temporary landing page for edge
url(r'^edge$', 'contentstore.views.edge', name='edge'),
@@ -83,6 +99,9 @@ urlpatterns = ('',
# User creation and updating views
urlpatterns += (
+ url(r'^(?P[^/]+)/(?P[^/]+)/checklists/(?P[^/]+)$', 'contentstore.views.get_checklists', name='checklists'),
+ url(r'^(?P[^/]+)/(?P[^/]+)/checklists/(?P[^/]+)/update(/)?(?P.+)?.*$',
+ 'contentstore.views.update_checklist', name='checklists_updates'),
url(r'^howitworks$', 'contentstore.views.howitworks', name='howitworks'),
url(r'^signup$', 'contentstore.views.signup', name='signup'),
@@ -100,12 +119,12 @@ urlpatterns += (
)
if settings.ENABLE_JASMINE:
- ## Jasmine
+ # # Jasmine
urlpatterns = urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
urlpatterns = patterns(*urlpatterns)
-#Custom error pages
+# Custom error pages
handler404 = 'contentstore.views.render_404'
handler500 = 'contentstore.views.render_500'
diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py
index c362ed4e89..7924012bfe 100644
--- a/common/djangoapps/course_groups/cohorts.py
+++ b/common/djangoapps/course_groups/cohorts.py
@@ -15,6 +15,24 @@ from .models import CourseUserGroup
log = logging.getLogger(__name__)
+# tl;dr: global state is bad. capa reseeds random every time a problem is loaded. Even
+# if and when that's fixed, it's a good idea to have a local generator to avoid any other
+# code that messes with the global random module.
+_local_random = None
+
+def local_random():
+ """
+ Get the local random number generator. In a function so that we don't run
+ random.Random() at import time.
+ """
+ # ironic, isn't it?
+ global _local_random
+
+ if _local_random is None:
+ _local_random = random.Random()
+
+ return _local_random
+
def is_course_cohorted(course_id):
"""
Given a course id, return a boolean for whether or not the course is
@@ -129,13 +147,7 @@ def get_cohort(user, course_id):
return None
# Put user in a random group, creating it if needed
- choice = random.randrange(0, n)
- group_name = choices[choice]
-
- # Victor: we are seeing very strange behavior on prod, where almost all users
- # end up in the same group. Log at INFO to try to figure out what's going on.
- log.info("DEBUG: adding user {0} to cohort {1}. choice={2}".format(
- user, group_name,choice))
+ group_name = local_random().choice(choices)
group, created = CourseUserGroup.objects.get_or_create(
course_id=course_id,
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index 54bdd77297..56b1293c2d 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -75,10 +75,15 @@ class UserProfile(models.Model):
GENDER_CHOICES = (('m', 'Male'), ('f', 'Female'), ('o', 'Other'))
gender = models.CharField(blank=True, null=True, max_length=6, db_index=True,
choices=GENDER_CHOICES)
- LEVEL_OF_EDUCATION_CHOICES = (('p_se', 'Doctorate in science or engineering'),
- ('p_oth', 'Doctorate in another field'),
+
+ # [03/21/2013] removed these, but leaving comment since there'll still be
+ # p_se and p_oth in the existing data in db.
+ # ('p_se', 'Doctorate in science or engineering'),
+ # ('p_oth', 'Doctorate in another field'),
+ LEVEL_OF_EDUCATION_CHOICES = (('p', 'Doctorate'),
('m', "Master's or professional degree"),
('b', "Bachelor's degree"),
+ ('a', "Associate's degree"),
('hs', "Secondary/high school"),
('jhs', "Junior secondary/junior high/middle school"),
('el', "Elementary/primary school"),
diff --git a/common/djangoapps/student/tests/__init__.py b/common/djangoapps/student/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/common/djangoapps/student/tests/factories.py b/common/djangoapps/student/tests/factories.py
new file mode 100644
index 0000000000..f74188725a
--- /dev/null
+++ b/common/djangoapps/student/tests/factories.py
@@ -0,0 +1,59 @@
+from student.models import (User, UserProfile, Registration,
+ CourseEnrollmentAllowed, CourseEnrollment)
+from django.contrib.auth.models import Group
+from datetime import datetime
+from factory import Factory, SubFactory
+from uuid import uuid4
+
+
+class GroupFactory(Factory):
+ FACTORY_FOR = Group
+
+ name = 'staff_MITx/999/Robot_Super_Course'
+
+
+class UserProfileFactory(Factory):
+ FACTORY_FOR = UserProfile
+
+ user = None
+ name = 'Robot Test'
+ level_of_education = None
+ gender = 'm'
+ mailing_address = None
+ goals = 'World domination'
+
+
+class RegistrationFactory(Factory):
+ FACTORY_FOR = Registration
+
+ user = None
+ activation_key = uuid4().hex
+
+
+class UserFactory(Factory):
+ FACTORY_FOR = User
+
+ username = 'robot'
+ email = 'robot+test@edx.org'
+ password = 'test'
+ first_name = 'Robot'
+ last_name = 'Test'
+ is_staff = False
+ is_active = True
+ is_superuser = False
+ last_login = datetime(2012, 1, 1)
+ date_joined = datetime(2011, 1, 1)
+
+
+class CourseEnrollmentFactory(Factory):
+ FACTORY_FOR = CourseEnrollment
+
+ user = SubFactory(UserFactory)
+ course_id = 'edX/toy/2012_Fall'
+
+
+class CourseEnrollmentAllowedFactory(Factory):
+ FACTORY_FOR = CourseEnrollmentAllowed
+
+ email = 'test@edx.org'
+ course_id = 'edX/test/2012_Fall'
diff --git a/common/djangoapps/student/tests.py b/common/djangoapps/student/tests/tests.py
similarity index 97%
rename from common/djangoapps/student/tests.py
rename to common/djangoapps/student/tests/tests.py
index 6a2d75e3d8..4638da44b2 100644
--- a/common/djangoapps/student/tests.py
+++ b/common/djangoapps/student/tests/tests.py
@@ -9,8 +9,8 @@ import logging
from django.test import TestCase
from mock import Mock
-from .models import unique_id_for_user
-from .views import process_survey_link, _cert_info
+from student.models import unique_id_for_user
+from student.views import process_survey_link, _cert_info
COURSE_1 = 'edX/toy/2012_Fall'
COURSE_2 = 'edx/full/6.002_Spring_2012'
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 902ec82677..5dbaf5d2c2 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -311,7 +311,7 @@ def change_enrollment(request):
course = course_from_id(course_id)
except ItemNotFoundError:
log.warning("User {0} tried to enroll in non-existent course {1}"
- .format(user.username, enrollment.course_id))
+ .format(user.username, course_id))
return {'success': False, 'error': 'The course requested does not exist.'}
if not has_access(user, course, 'enroll'):
diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py
index 6394959532..c8cc0c9e4b 100644
--- a/common/djangoapps/terrain/browser.py
+++ b/common/djangoapps/terrain/browser.py
@@ -1,7 +1,6 @@
from lettuce import before, after, world
from splinter.browser import Browser
from logging import getLogger
-import time
# Let the LMS and CMS do their one-time setup
# For example, setting up mongo caches
@@ -16,6 +15,9 @@ from django.core.management import call_command
@before.harvest
def initial_setup(server):
+ '''
+ Launch the browser once before executing the tests
+ '''
# Launch the browser app (choose one of these below)
world.browser = Browser('chrome')
# world.browser = Browser('phantomjs')
@@ -24,14 +26,18 @@ def initial_setup(server):
@before.each_scenario
def reset_data(scenario):
- # Clean out the django test database defined in the
- # envs/acceptance.py file: mitx_all/db/test_mitx.db
+ '''
+ Clean out the django test database defined in the
+ envs/acceptance.py file: mitx_all/db/test_mitx.db
+ '''
logger.debug("Flushing the test database...")
call_command('flush', interactive=False)
@after.all
def teardown_browser(total):
- # Quit firefox
+ '''
+ Quit the browser after executing the tests
+ '''
world.browser.quit()
pass
diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py
index c36bf935f1..768c51b25e 100644
--- a/common/djangoapps/terrain/factories.py
+++ b/common/djangoapps/terrain/factories.py
@@ -1,190 +1,64 @@
-from student.models import User, UserProfile, Registration
-from django.contrib.auth.models import Group
-from datetime import datetime
-from factory import Factory
-from xmodule.modulestore import Location
-from xmodule.modulestore.django import modulestore
-from time import gmtime
-from uuid import uuid4
-from xmodule.timeparse import stringify_time
-from xmodule.modulestore.inheritance import own_metadata
+'''
+Factories are defined in other modules and absorbed here into the
+lettuce world so that they can be used by both unit tests
+and integration / BDD tests.
+'''
+import student.tests.factories as sf
+import xmodule.modulestore.tests.factories as xf
+from lettuce import world
-class GroupFactory(Factory):
- FACTORY_FOR = Group
-
- name = 'staff_MITx/999/Robot_Super_Course'
-
-
-class UserProfileFactory(Factory):
- FACTORY_FOR = UserProfile
-
- user = None
- name = 'Robot Test'
- level_of_education = None
- gender = 'm'
- mailing_address = None
- goals = 'World domination'
-
-
-class RegistrationFactory(Factory):
- FACTORY_FOR = Registration
-
- user = None
- activation_key = uuid4().hex
-
-
-class UserFactory(Factory):
- FACTORY_FOR = User
-
- username = 'robot'
- email = 'robot+test@edx.org'
- password = 'test'
- first_name = 'Robot'
- last_name = 'Test'
- is_staff = False
- is_active = True
- is_superuser = False
- last_login = datetime(2012, 1, 1)
- date_joined = datetime(2011, 1, 1)
-
-
-def XMODULE_COURSE_CREATION(class_to_create, **kwargs):
- return XModuleCourseFactory._create(class_to_create, **kwargs)
-
-
-def XMODULE_ITEM_CREATION(class_to_create, **kwargs):
- return XModuleItemFactory._create(class_to_create, **kwargs)
-
-
-class XModuleCourseFactory(Factory):
+@world.absorb
+class UserFactory(sf.UserFactory):
"""
- Factory for XModule courses.
+ User account for lms / cms
"""
-
- ABSTRACT_FACTORY = True
- _creation_function = (XMODULE_COURSE_CREATION,)
-
- @classmethod
- def _create(cls, target_class, *args, **kwargs):
-
- template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
- org = kwargs.get('org')
- number = kwargs.get('number')
- display_name = kwargs.get('display_name')
- location = Location('i4x', org, number,
- 'course', Location.clean(display_name))
-
- store = modulestore('direct')
-
- # Write the data to the mongo datastore
- new_course = store.clone_item(template, location)
-
- # This metadata code was copied from cms/djangoapps/contentstore/views.py
- if display_name is not None:
- new_course.display_name = display_name
-
- new_course.lms.start = gmtime()
- new_course.tabs = [{"type": "courseware"},
- {"type": "course_info", "name": "Course Info"},
- {"type": "discussion", "name": "Discussion"},
- {"type": "wiki", "name": "Wiki"},
- {"type": "progress", "name": "Progress"}]
-
- # Update the data in the mongo datastore
- store.update_metadata(new_course.location.url(), own_metadata(new_course))
-
- return new_course
-
-
-class Course:
pass
-class CourseFactory(XModuleCourseFactory):
- FACTORY_FOR = Course
-
- template = 'i4x://edx/templates/course/Empty'
- org = 'MITx'
- number = '999'
- display_name = 'Robot Super Course'
-
-
-class XModuleItemFactory(Factory):
+@world.absorb
+class UserProfileFactory(sf.UserProfileFactory):
"""
- Factory for XModule items.
+ Demographics etc for the User
"""
-
- ABSTRACT_FACTORY = True
- _creation_function = (XMODULE_ITEM_CREATION,)
-
- @classmethod
- def _create(cls, target_class, *args, **kwargs):
- """
- Uses *kwargs*:
-
- *parent_location* (required): the location of the parent module
- (e.g. the parent course or section)
-
- *template* (required): the template to create the item from
- (e.g. i4x://templates/section/Empty)
-
- *data* (optional): the data for the item
- (e.g. XML problem definition for a problem item)
-
- *display_name* (optional): the display name of the item
-
- *metadata* (optional): dictionary of metadata attributes
-
- *target_class* is ignored
- """
-
- DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
-
- parent_location = Location(kwargs.get('parent_location'))
- template = Location(kwargs.get('template'))
- data = kwargs.get('data')
- display_name = kwargs.get('display_name')
- metadata = kwargs.get('metadata', {})
-
- store = modulestore('direct')
-
- # This code was based off that in cms/djangoapps/contentstore/views.py
- parent = store.get_item(parent_location)
-
- # If a display name is set, use that
- dest_name = display_name.replace(" ", "_") if display_name is not None else uuid4().hex
- dest_location = parent_location._replace(category=template.category,
- name=dest_name)
-
- new_item = store.clone_item(template, dest_location)
-
- # replace the display name with an optional parameter passed in from the caller
- if display_name is not None:
- new_item.display_name = display_name
-
- # Add additional metadata or override current metadata
- item_metadata = own_metadata(new_item)
- item_metadata.update(metadata)
- store.update_metadata(new_item.location.url(), item_metadata)
-
- # replace the data with the optional *data* parameter
- if data is not None:
- store.update_item(new_item.location, data)
-
- if new_item.location.category not in DETACHED_CATEGORIES:
- store.update_children(parent_location, parent.children + [new_item.location.url()])
-
- return new_item
-
-
-class Item:
pass
-class ItemFactory(XModuleItemFactory):
- FACTORY_FOR = Item
+@world.absorb
+class RegistrationFactory(sf.RegistrationFactory):
+ """
+ Activation key for registering the user account
+ """
+ pass
- parent_location = 'i4x://MITx/999/course/Robot_Super_Course'
- template = 'i4x://edx/templates/chapter/Empty'
- display_name = 'Section One'
+
+@world.absorb
+class GroupFactory(sf.GroupFactory):
+ """
+ Groups for user permissions for courses
+ """
+ pass
+
+
+@world.absorb
+class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowed):
+ """
+ Users allowed to enroll in the course outside of the usual window
+ """
+ pass
+
+
+@world.absorb
+class CourseFactory(xf.CourseFactory):
+ """
+ Courseware courses
+ """
+ pass
+
+
+@world.absorb
+class ItemFactory(xf.ItemFactory):
+ """
+ Everything included inside a course
+ """
+ pass
diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py
index 52eeb23c4a..3bc838a6af 100644
--- a/common/djangoapps/terrain/steps.py
+++ b/common/djangoapps/terrain/steps.py
@@ -1,7 +1,12 @@
from lettuce import world, step
from .factories import *
from lettuce.django import django_url
+from django.conf import settings
+from django.http import HttpRequest
from django.contrib.auth.models import User
+from django.contrib.auth import authenticate, login
+from django.contrib.auth.middleware import AuthenticationMiddleware
+from django.contrib.sessions.middleware import SessionMiddleware
from student.models import CourseEnrollment
from urllib import quote_plus
from nose.tools import assert_equals
@@ -9,6 +14,7 @@ from bs4 import BeautifulSoup
import time
import re
import os.path
+from selenium.common.exceptions import WebDriverException
from logging import getLogger
logger = getLogger(__name__)
@@ -24,6 +30,11 @@ def reload_the_page(step):
world.browser.reload()
+@step('I press the browser back button$')
+def browser_back(step):
+ world.browser.driver.back()
+
+
@step('I (?:visit|access|open) the homepage$')
def i_visit_the_homepage(step):
world.browser.visit(django_url('/'))
@@ -77,7 +88,7 @@ def the_page_title_should_contain(step, title):
@step('I am a logged in user$')
def i_am_logged_in_user(step):
create_user('robot')
- log_in('robot@edx.org', 'test')
+ log_in('robot', 'test')
@step('I am not logged in$')
@@ -92,7 +103,7 @@ def i_am_staff_for_course_by_id(step, course_id):
@step('I log in$')
def i_log_in(step):
- log_in('robot@edx.org', 'test')
+ log_in('robot', 'test')
@step(u'I am an edX user$')
@@ -119,38 +130,46 @@ def create_user(uname):
portal_user.set_password('test')
portal_user.save()
- registration = RegistrationFactory(user=portal_user)
+ registration = world.RegistrationFactory(user=portal_user)
registration.register(portal_user)
registration.activate()
- user_profile = UserProfileFactory(user=portal_user)
+ user_profile = world.UserProfileFactory(user=portal_user)
@world.absorb
-def log_in(email, password):
- world.browser.cookies.delete()
- world.browser.visit(django_url('/'))
- world.browser.is_element_present_by_css('header.global', 10)
- world.browser.click_link_by_href('#login-modal')
+def log_in(username, password):
+ '''
+ Log the user in programatically
+ '''
- # Wait for the login dialog to load
- # This is complicated by the fact that sometimes a second #login_form
- # dialog loads, while the first one remains hidden.
- # We give them both time to load, starting with the second one.
- world.browser.is_element_present_by_css('section.content-wrapper form#login_form', wait_time=4)
- world.browser.is_element_present_by_css('form#login_form', wait_time=2)
+ # Authenticate the user
+ user = authenticate(username=username, password=password)
+ assert(user is not None and user.is_active)
- # For some reason, the page sometimes includes two #login_form
- # elements, the first of which is not visible.
- # To avoid this, we always select the last of the two #login_form dialogs
- login_form = world.browser.find_by_css('form#login_form').last
+ # Send a fake HttpRequest to log the user in
+ # We need to process the request using
+ # Session middleware and Authentication middleware
+ # to ensure that session state can be stored
+ request = HttpRequest()
+ SessionMiddleware().process_request(request)
+ AuthenticationMiddleware().process_request(request)
+ login(request, user)
- login_form.find_by_name('email').fill(email)
- login_form.find_by_name('password').fill(password)
- login_form.find_by_name('submit').click()
+ # Save the session
+ request.session.save()
- # wait for the page to redraw
- assert world.browser.is_element_present_by_css('.content-wrapper', wait_time=10)
+ # Retrieve the sessionid and add it to the browser's cookies
+ cookie_dict = {settings.SESSION_COOKIE_NAME: request.session.session_key}
+ try:
+ world.browser.cookies.add(cookie_dict)
+
+ # WebDriver has an issue where we cannot set cookies
+ # before we make a GET request, so if we get an error,
+ # we load the '/' page and try again
+ except:
+ world.browser.visit(django_url('/'))
+ world.browser.cookies.add(cookie_dict)
@world.absorb
@@ -207,6 +226,7 @@ def save_the_course_content(path='/tmp'):
u = world.browser.url
section_url = u[u.find('courseware/') + 11:]
+
if not os.path.exists(path):
os.makedirs(path)
@@ -214,3 +234,15 @@ def save_the_course_content(path='/tmp'):
f = open('%s/%s' % (path, filename), 'w')
f.write(output)
f.close
+
+@world.absorb
+def css_click(css_selector):
+ try:
+ world.browser.find_by_css(css_selector).click()
+
+ except WebDriverException:
+ # Occassionally, MathJax or other JavaScript can cover up
+ # an element temporarily.
+ # If this happens, wait a second, then try again
+ time.sleep(1)
+ world.browser.find_by_css(css_selector).click()
diff --git a/common/djangoapps/util/converters.py b/common/djangoapps/util/converters.py
index ec2d29ecfa..212cceb77d 100644
--- a/common/djangoapps/util/converters.py
+++ b/common/djangoapps/util/converters.py
@@ -1,17 +1,25 @@
import time
import datetime
-import re
import calendar
+import dateutil.parser
def time_to_date(time_obj):
"""
- Convert a time.time_struct to a true universal time (can pass to js Date constructor)
+ Convert a time.time_struct to a true universal time (can pass to js Date
+ constructor)
"""
- # TODO change to using the isoformat() function on datetime. js date can parse those
return calendar.timegm(time_obj) * 1000
+def time_to_isodate(source):
+ '''Convert to an iso date'''
+ if isinstance(source, time.struct_time):
+ return time.strftime('%Y-%m-%dT%H:%M:%SZ', source)
+ elif isinstance(source, datetime):
+ return source.isoformat() + 'Z'
+
+
def jsdate_to_time(field):
"""
Convert a universal time (iso format) or msec since epoch to a time obj
@@ -19,8 +27,7 @@ def jsdate_to_time(field):
if field is None:
return field
elif isinstance(field, basestring):
- # ISO format but ignores time zone assuming it's Z.
- d = datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable
+ d = dateutil.parser.parse(field)
return d.utctimetuple()
elif isinstance(field, (int, long, float)):
return time.gmtime(field / 1000)
diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py
index e024909d75..0c007f83b2 100644
--- a/common/lib/capa/capa/tests/test_responsetypes.py
+++ b/common/lib/capa/capa/tests/test_responsetypes.py
@@ -17,6 +17,7 @@ from capa.correctmap import CorrectMap
from capa.util import convert_files_to_filenames
from capa.xqueue_interface import dateformat
+
class ResponseTest(unittest.TestCase):
""" Base class for tests of capa responses."""
@@ -39,12 +40,13 @@ class ResponseTest(unittest.TestCase):
for input_str in correct_answers:
result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1')
self.assertEqual(result, 'correct',
- msg="%s should be marked correct" % str(input_str))
+ msg="%s should be marked correct" % str(input_str))
for input_str in incorrect_answers:
result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1')
self.assertEqual(result, 'incorrect',
- msg="%s should be marked incorrect" % str(input_str))
+ msg="%s should be marked incorrect" % str(input_str))
+
class MultiChoiceResponseTest(ResponseTest):
from response_xml_factory import MultipleChoiceResponseXMLFactory
@@ -60,7 +62,7 @@ class MultiChoiceResponseTest(ResponseTest):
def test_named_multiple_choice_grade(self):
problem = self.build_problem(choices=[False, True, False],
- choice_names=["foil_1", "foil_2", "foil_3"])
+ choice_names=["foil_1", "foil_2", "foil_3"])
# Ensure that we get the expected grades
self.assert_grade(problem, 'choice_foil_1', 'incorrect')
@@ -91,7 +93,7 @@ class TrueFalseResponseTest(ResponseTest):
def test_named_true_false_grade(self):
problem = self.build_problem(choices=[False, True, True],
- choice_names=['foil_1','foil_2','foil_3'])
+ choice_names=['foil_1', 'foil_2', 'foil_3'])
# Check the results
# Mark correct if and only if ALL (and only) correct chocies selected
@@ -107,6 +109,7 @@ class TrueFalseResponseTest(ResponseTest):
self.assert_grade(problem, 'choice_foil_4', 'incorrect')
self.assert_grade(problem, 'not_a_choice', 'incorrect')
+
class ImageResponseTest(ResponseTest):
from response_xml_factory import ImageResponseXMLFactory
xml_factory_class = ImageResponseXMLFactory
@@ -118,7 +121,7 @@ class ImageResponseTest(ResponseTest):
# Anything inside the rectangle (and along the borders) is correct
# Everything else is incorrect
correct_inputs = ["[12,19]", "[10,10]", "[20,20]",
- "[10,15]", "[20,15]", "[15,10]", "[15,20]"]
+ "[10,15]", "[20,15]", "[15,10]", "[15,20]"]
incorrect_inputs = ["[4,6]", "[25,15]", "[15,40]", "[15,4]"]
self.assert_multiple_grade(problem, correct_inputs, incorrect_inputs)
@@ -145,7 +148,7 @@ class ImageResponseTest(ResponseTest):
def test_multiple_regions_grade(self):
# Define multiple regions that the user can select
- region_str="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"
+ region_str = "[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"
# Expect that only points inside the regions are marked correct
problem = self.build_problem(regions=region_str)
@@ -155,7 +158,7 @@ class ImageResponseTest(ResponseTest):
def test_region_and_rectangle_grade(self):
rectangle_str = "(100,100)-(200,200)"
- region_str="[[10,10], [20,10], [20, 30]]"
+ region_str = "[[10,10], [20,10], [20, 30]]"
# Expect that only points inside the rectangle or region are marked correct
problem = self.build_problem(regions=region_str, rectangle=rectangle_str)
@@ -171,85 +174,85 @@ class SymbolicResponseTest(unittest.TestCase):
test_lcp = lcp.LoncapaProblem(open(symbolicresponse_file).read(), '1', system=test_system)
correct_answers = {'1_2_1': 'cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]',
'1_2_1_dynamath': '''
-
-''',
+
+ ''',
}
wrong_answers = {'1_2_1': '2',
'1_2_1_dynamath': '''
''',
- }
+
+ 2
+
+ ''',
+ }
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
self.assertEquals(test_lcp.grade_answers(wrong_answers).get_correctness('1_2_1'), 'incorrect')
@@ -260,7 +263,7 @@ class OptionResponseTest(ResponseTest):
def test_grade(self):
problem = self.build_problem(options=["first", "second", "third"],
- correct_option="second")
+ correct_option="second")
# Assert that we get the expected grades
self.assert_grade(problem, "first", "incorrect")
@@ -281,9 +284,9 @@ class FormulaResponseTest(ResponseTest):
# The expected solution is numerically equivalent to x+2y
problem = self.build_problem(sample_dict=sample_dict,
- num_samples=10,
- tolerance=0.01,
- answer="x+2*y")
+ num_samples=10,
+ tolerance=0.01,
+ answer="x+2*y")
# Expect an equivalent formula to be marked correct
# 2x - x + y + y = x + 2y
@@ -297,33 +300,31 @@ class FormulaResponseTest(ResponseTest):
def test_hint(self):
# Sample variables x and y in the range [-10, 10]
- sample_dict = {'x': (-10, 10), 'y': (-10,10) }
+ sample_dict = {'x': (-10, 10), 'y': (-10, 10)}
# Give a hint if the user leaves off the coefficient
# or leaves out x
hints = [('x + 3*y', 'y_coefficient', 'Check the coefficient of y'),
- ('2*y', 'missing_x', 'Try including the variable x')]
-
+ ('2*y', 'missing_x', 'Try including the variable x')]
# The expected solution is numerically equivalent to x+2y
problem = self.build_problem(sample_dict=sample_dict,
- num_samples=10,
- tolerance=0.01,
- answer="x+2*y",
- hints=hints)
+ num_samples=10,
+ tolerance=0.01,
+ answer="x+2*y",
+ hints=hints)
# Expect to receive a hint if we add an extra y
input_dict = {'1_2_1': "x + 2*y + y"}
correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'),
- 'Check the coefficient of y')
+ 'Check the coefficient of y')
# Expect to receive a hint if we leave out x
input_dict = {'1_2_1': "2*y"}
correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'),
- 'Try including the variable x')
-
+ 'Try including the variable x')
def test_script(self):
# Calculate the answer using a script
@@ -334,10 +335,10 @@ class FormulaResponseTest(ResponseTest):
# The expected solution is numerically equivalent to 2*x
problem = self.build_problem(sample_dict=sample_dict,
- num_samples=10,
- tolerance=0.01,
- answer="$calculated_ans",
- script=script)
+ num_samples=10,
+ tolerance=0.01,
+ answer="$calculated_ans",
+ script=script)
# Expect that the inputs are graded correctly
self.assert_grade(problem, '2*x', 'correct')
@@ -348,7 +349,6 @@ class StringResponseTest(ResponseTest):
from response_xml_factory import StringResponseXMLFactory
xml_factory_class = StringResponseXMLFactory
-
def test_case_sensitive(self):
problem = self.build_problem(answer="Second", case_sensitive=True)
@@ -372,23 +372,23 @@ class StringResponseTest(ResponseTest):
def test_hints(self):
hints = [("wisconsin", "wisc", "The state capital of Wisconsin is Madison"),
- ("minnesota", "minn", "The state capital of Minnesota is St. Paul")]
+ ("minnesota", "minn", "The state capital of Minnesota is St. Paul")]
problem = self.build_problem(answer="Michigan",
- case_sensitive=False,
- hints=hints)
+ case_sensitive=False,
+ hints=hints)
# We should get a hint for Wisconsin
input_dict = {'1_2_1': 'Wisconsin'}
correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'),
- "The state capital of Wisconsin is Madison")
+ "The state capital of Wisconsin is Madison")
# We should get a hint for Minnesota
input_dict = {'1_2_1': 'Minnesota'}
correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'),
- "The state capital of Minnesota is St. Paul")
+ "The state capital of Minnesota is St. Paul")
# We should NOT get a hint for Michigan (the correct answer)
input_dict = {'1_2_1': 'Michigan'}
@@ -400,6 +400,7 @@ class StringResponseTest(ResponseTest):
correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'), "")
+
class CodeResponseTest(ResponseTest):
from response_xml_factory import CodeResponseXMLFactory
xml_factory_class = CodeResponseXMLFactory
@@ -409,9 +410,9 @@ class CodeResponseTest(ResponseTest):
grader_payload = json.dumps({"grader": "ps04/grade_square.py"})
self.problem = self.build_problem(initial_display="def square(x):",
- answer_display="answer",
- grader_payload=grader_payload,
- num_responses=2)
+ answer_display="answer",
+ grader_payload=grader_payload,
+ num_responses=2)
@staticmethod
def make_queuestate(key, time):
@@ -442,7 +443,6 @@ class CodeResponseTest(ResponseTest):
self.assertEquals(self.problem.is_queued(), True)
-
def test_update_score(self):
'''
Test whether LoncapaProblem.update_score can deliver queued result to the right subproblem
@@ -495,7 +495,6 @@ class CodeResponseTest(ResponseTest):
else:
self.assertTrue(self.problem.correct_map.is_queued(test_id)) # Should be queued, message undelivered
-
def test_recentmost_queuetime(self):
'''
Test whether the LoncapaProblem knows about the time of queue requests
@@ -538,13 +537,14 @@ class CodeResponseTest(ResponseTest):
self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3'])
self.assertEquals(answers_converted['1_4_1'], [fp.name, fp.name])
+
class ChoiceResponseTest(ResponseTest):
from response_xml_factory import ChoiceResponseXMLFactory
xml_factory_class = ChoiceResponseXMLFactory
def test_radio_group_grade(self):
problem = self.build_problem(choice_type='radio',
- choices=[False, True, False])
+ choices=[False, True, False])
# Check that we get the expected results
self.assert_grade(problem, 'choice_0', 'incorrect')
@@ -554,10 +554,9 @@ class ChoiceResponseTest(ResponseTest):
# No choice 3 exists --> mark incorrect
self.assert_grade(problem, 'choice_3', 'incorrect')
-
def test_checkbox_group_grade(self):
problem = self.build_problem(choice_type='checkbox',
- choices=[False, True, True])
+ choices=[False, True, True])
# Check that we get the expected results
# (correct if and only if BOTH correct choices chosen)
@@ -581,14 +580,15 @@ class JavascriptResponseTest(ResponseTest):
os.system("coffee -c %s" % (coffee_file_path))
problem = self.build_problem(generator_src="test_problem_generator.js",
- grader_src="test_problem_grader.js",
- display_class="TestProblemDisplay",
- display_src="test_problem_display.js",
- param_dict={'value': '4'})
+ grader_src="test_problem_grader.js",
+ display_class="TestProblemDisplay",
+ display_src="test_problem_display.js",
+ param_dict={'value': '4'})
# Test that we get graded correctly
- self.assert_grade(problem, json.dumps({0:4}), "correct")
- self.assert_grade(problem, json.dumps({0:5}), "incorrect")
+ self.assert_grade(problem, json.dumps({0: 4}), "correct")
+ self.assert_grade(problem, json.dumps({0: 5}), "incorrect")
+
class NumericalResponseTest(ResponseTest):
from response_xml_factory import NumericalResponseXMLFactory
@@ -596,27 +596,26 @@ class NumericalResponseTest(ResponseTest):
def test_grade_exact(self):
problem = self.build_problem(question_text="What is 2 + 2?",
- explanation="The answer is 4",
- answer=4)
+ explanation="The answer is 4",
+ answer=4)
correct_responses = ["4", "4.0", "4.00"]
incorrect_responses = ["", "3.9", "4.1", "0"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
-
def test_grade_decimal_tolerance(self):
problem = self.build_problem(question_text="What is 2 + 2 approximately?",
- explanation="The answer is 4",
- answer=4,
- tolerance=0.1)
+ explanation="The answer is 4",
+ answer=4,
+ tolerance=0.1)
correct_responses = ["4.0", "4.00", "4.09", "3.91"]
incorrect_responses = ["", "4.11", "3.89", "0"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
def test_grade_percent_tolerance(self):
problem = self.build_problem(question_text="What is 2 + 2 approximately?",
- explanation="The answer is 4",
- answer=4,
- tolerance="10%")
+ explanation="The answer is 4",
+ answer=4,
+ tolerance="10%")
correct_responses = ["4.0", "4.3", "3.7", "4.30", "3.70"]
incorrect_responses = ["", "4.5", "3.5", "0"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
@@ -624,9 +623,9 @@ class NumericalResponseTest(ResponseTest):
def test_grade_with_script(self):
script_text = "computed_response = math.sqrt(4)"
problem = self.build_problem(question_text="What is sqrt(4)?",
- explanation="The answer is 2",
- answer="$computed_response",
- script=script_text)
+ explanation="The answer is 2",
+ answer="$computed_response",
+ script=script_text)
correct_responses = ["2", "2.0"]
incorrect_responses = ["", "2.01", "1.99", "0"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
@@ -634,10 +633,10 @@ class NumericalResponseTest(ResponseTest):
def test_grade_with_script_and_tolerance(self):
script_text = "computed_response = math.sqrt(4)"
problem = self.build_problem(question_text="What is sqrt(4)?",
- explanation="The answer is 2",
- answer="$computed_response",
- tolerance="0.1",
- script=script_text)
+ explanation="The answer is 2",
+ answer="$computed_response",
+ tolerance="0.1",
+ script=script_text)
correct_responses = ["2", "2.0", "2.05", "1.95"]
incorrect_responses = ["", "2.11", "1.89", "0"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
@@ -651,7 +650,6 @@ class NumericalResponseTest(ResponseTest):
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
-
class CustomResponseTest(ResponseTest):
from response_xml_factory import CustomResponseXMLFactory
xml_factory_class = CustomResponseXMLFactory
@@ -692,7 +690,6 @@ class CustomResponseTest(ResponseTest):
overall_msg = correctmap.get_overall_message()
self.assertEqual(overall_msg, "Overall message")
-
def test_function_code_single_input(self):
# For function code, we pass in these arguments:
@@ -746,7 +743,7 @@ class CustomResponseTest(ResponseTest):
""")
problem = self.build_problem(script=script, cfn="check_func",
- expect="42", num_inputs=2)
+ expect="42", num_inputs=2)
# Correct answer -- expect both inputs marked correct
input_dict = {'1_2_1': '42', '1_2_2': '42'}
@@ -768,7 +765,6 @@ class CustomResponseTest(ResponseTest):
correctness = correct_map.get_correctness('1_2_2')
self.assertEqual(correctness, 'incorrect')
-
def test_function_code_multiple_inputs(self):
# If the has multiple inputs associated with it,
@@ -794,10 +790,10 @@ class CustomResponseTest(ResponseTest):
""")
problem = self.build_problem(script=script,
- cfn="check_func", num_inputs=3)
+ cfn="check_func", num_inputs=3)
# Grade the inputs (one input incorrect)
- input_dict = {'1_2_1': '-999', '1_2_2': '2', '1_2_3': '3' }
+ input_dict = {'1_2_1': '-999', '1_2_2': '2', '1_2_3': '3'}
correct_map = problem.grade_answers(input_dict)
# Expect that we receive the overall message (for the whole response)
@@ -813,7 +809,6 @@ class CustomResponseTest(ResponseTest):
self.assertEqual(correct_map.get_msg('1_2_2'), 'Feedback 2')
self.assertEqual(correct_map.get_msg('1_2_3'), 'Feedback 3')
-
def test_multiple_inputs_return_one_status(self):
# When given multiple inputs, the 'answer_given' argument
# to the check_func() is a list of inputs
@@ -835,10 +830,10 @@ class CustomResponseTest(ResponseTest):
""")
problem = self.build_problem(script=script,
- cfn="check_func", num_inputs=3)
+ cfn="check_func", num_inputs=3)
# Grade the inputs (one input incorrect)
- input_dict = {'1_2_1': '-999', '1_2_2': '2', '1_2_3': '3' }
+ input_dict = {'1_2_1': '-999', '1_2_2': '2', '1_2_3': '3'}
correct_map = problem.grade_answers(input_dict)
# Everything marked incorrect
@@ -847,7 +842,7 @@ class CustomResponseTest(ResponseTest):
self.assertEqual(correct_map.get_correctness('1_2_3'), 'incorrect')
# Grade the inputs (everything correct)
- input_dict = {'1_2_1': '1', '1_2_2': '2', '1_2_3': '3' }
+ input_dict = {'1_2_1': '1', '1_2_2': '2', '1_2_3': '3'}
correct_map = problem.grade_answers(input_dict)
# Everything marked incorrect
@@ -902,13 +897,13 @@ class SchematicResponseTest(ResponseTest):
# To test that the context is set up correctly,
# we create a script that sets *correct* to true
# if and only if we find the *submission* (list)
- script="correct = ['correct' if 'test' in submission[0] else 'incorrect']"
+ script = "correct = ['correct' if 'test' in submission[0] else 'incorrect']"
problem = self.build_problem(answer=script)
# The actual dictionary would contain schematic information
# sent from the JavaScript simulation
submission_dict = {'test': 'test'}
- input_dict = { '1_2_1': json.dumps(submission_dict) }
+ input_dict = {'1_2_1': json.dumps(submission_dict)}
correct_map = problem.grade_answers(input_dict)
# Expect that the problem is graded as true
@@ -916,6 +911,7 @@ class SchematicResponseTest(ResponseTest):
# is what we expect)
self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct')
+
class AnnotationResponseTest(ResponseTest):
from response_xml_factory import AnnotationResponseXMLFactory
xml_factory_class = AnnotationResponseXMLFactory
@@ -924,18 +920,18 @@ class AnnotationResponseTest(ResponseTest):
(correct, partially, incorrect) = ('correct', 'partially-correct', 'incorrect')
answer_id = '1_2_1'
- options = (('x', correct),('y', partially),('z', incorrect))
- make_answer = lambda option_ids: {answer_id: json.dumps({'options': option_ids })}
+ options = (('x', correct), ('y', partially), ('z', incorrect))
+ make_answer = lambda option_ids: {answer_id: json.dumps({'options': option_ids})}
tests = [
- {'correctness': correct, 'points': 2,'answers': make_answer([0]) },
- {'correctness': partially, 'points': 1, 'answers': make_answer([1]) },
- {'correctness': incorrect, 'points': 0, 'answers': make_answer([2]) },
- {'correctness': incorrect, 'points': 0, 'answers': make_answer([0,1,2]) },
- {'correctness': incorrect, 'points': 0, 'answers': make_answer([]) },
- {'correctness': incorrect, 'points': 0, 'answers': make_answer('') },
- {'correctness': incorrect, 'points': 0, 'answers': make_answer(None) },
- {'correctness': incorrect, 'points': 0, 'answers': {answer_id: 'null' } },
+ {'correctness': correct, 'points': 2, 'answers': make_answer([0])},
+ {'correctness': partially, 'points': 1, 'answers': make_answer([1])},
+ {'correctness': incorrect, 'points': 0, 'answers': make_answer([2])},
+ {'correctness': incorrect, 'points': 0, 'answers': make_answer([0, 1, 2])},
+ {'correctness': incorrect, 'points': 0, 'answers': make_answer([])},
+ {'correctness': incorrect, 'points': 0, 'answers': make_answer('')},
+ {'correctness': incorrect, 'points': 0, 'answers': make_answer(None)},
+ {'correctness': incorrect, 'points': 0, 'answers': {answer_id: 'null'}},
]
for (index, test) in enumerate(tests):
diff --git a/common/lib/xmodule/xmodule/conditional_module.py b/common/lib/xmodule/xmodule/conditional_module.py
index a9375cae78..b3e0e0e06b 100644
--- a/common/lib/xmodule/xmodule/conditional_module.py
+++ b/common/lib/xmodule/xmodule/conditional_module.py
@@ -40,8 +40,21 @@ class ConditionalModule(ConditionalFields, XModule):
poll_answer - map to `poll_answer` module attribute
voted - map to `voted` module attribute
- tag attributes:
- sources - location id of modules, separated by ';'
+ tag attributes:
+ sources - location id of required modules, separated by ';'
+
+ You can add you own rules for tag, like
+ "completed", "attempted" etc. To do that yo must extend
+ `ConditionalModule.conditions_map` variable and add pair:
+ my_attr: my_property/my_method
+
+ After that you can use it:
+
+ ...
+
+
+ And my_property/my_method will be called for required modules.
+
"""
js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'),
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 7c47e0887a..b1e5fa02c8 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -1,6 +1,6 @@
import logging
from cStringIO import StringIO
-from math import exp, erf
+from math import exp
from lxml import etree
from path import path # NOTE (THK): Only used for detecting presence of syllabus
import requests
@@ -12,14 +12,9 @@ from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.timeparse import parse_time
from xmodule.util.decorators import lazyproperty
from xmodule.graders import grader_from_conf
-from datetime import datetime
import json
-import logging
-import requests
-import time
-import copy
-from xblock.core import Scope, ModelType, List, String, Object, Boolean
+from xblock.core import Scope, List, String, Object, Boolean
from .fields import Date
@@ -29,30 +24,30 @@ log = logging.getLogger(__name__)
class StringOrDate(Date):
def from_json(self, value):
"""
- Parse an optional metadata key containing a time: if present, complain
- if it doesn't parse.
- Return None if not present or invalid.
+ Parse an optional metadata key containing a time or a string:
+ if present, assume it's a string if it doesn't parse.
"""
- if value is None:
- return None
-
try:
- return time.strptime(value, self.time_format)
+ result = super(StringOrDate, self).from_json(value)
except ValueError:
return value
+ if result is None:
+ return value
+ else:
+ return result
def to_json(self, value):
"""
- Convert a time struct to a string
+ Convert a time struct or string to a string.
"""
- if value is None:
- return None
-
try:
- return time.strftime(self.time_format, value)
- except (ValueError, TypeError):
+ result = super(StringOrDate, self).to_json(value)
+ except:
return value
-
+ if result is None:
+ return value
+ else:
+ return result
edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
@@ -60,6 +55,7 @@ edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
_cached_toc = {}
+
class Textbook(object):
def __init__(self, title, book_url):
self.title = title
@@ -179,7 +175,7 @@ class CourseFields(object):
allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False)
advanced_modules = List(help="Beta modules used in your course", scope=Scope.settings)
has_children = True
-
+ checklists = List(scope=Scope.settings)
info_sidebar_name = String(scope=Scope.settings, default='Course Handouts')
# An extra property is used rather than the wiki_slug/number because
@@ -367,7 +363,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
textbooks.append((textbook.get('title'), textbook.get('book_url')))
xml_object.remove(textbook)
- #Load the wiki tag if it exists
+ # Load the wiki tag if it exists
wiki_slug = None
wiki_tag = xml_object.find("wiki")
if wiki_tag is not None:
@@ -675,7 +671,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
# *end* of the same day, not the same time. It's going to be used as the
# end of the exam overall, so we don't want the exam to disappear too soon.
# It's also used optionally as the registration end date, so time matters there too.
- self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date
+ self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date
if self.last_eligible_appointment_date is None:
raise ValueError("Last appointment date must be specified")
self.registration_start_date = self._try_parse_time('Registration_Start_Date') or time.gmtime(0)
diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py
index fb80752e56..99ead854ad 100644
--- a/common/lib/xmodule/xmodule/fields.py
+++ b/common/lib/xmodule/xmodule/fields.py
@@ -4,27 +4,35 @@ import re
from datetime import timedelta
from xblock.core import ModelType
+import datetime
+import dateutil.parser
log = logging.getLogger(__name__)
class Date(ModelType):
- time_format = "%Y-%m-%dT%H:%M"
-
- def from_json(self, value):
+ '''
+ Date fields know how to parse and produce json (iso) compatible formats.
+ '''
+ # NB: these are copies of util.converters.*
+ def from_json(self, field):
"""
Parse an optional metadata key containing a time: if present, complain
if it doesn't parse.
Return None if not present or invalid.
"""
- if value is None:
- return None
-
- try:
- return time.strptime(value, self.time_format)
- except ValueError as e:
- msg = "Field {0} has bad value '{1}': '{2}'".format(
- self._name, value, e)
+ if field is None:
+ return field
+ elif isinstance(field, basestring):
+ d = dateutil.parser.parse(field)
+ return d.utctimetuple()
+ elif isinstance(field, (int, long, float)):
+ return time.gmtime(field / 1000)
+ elif isinstance(field, time.struct_time):
+ return field
+ else:
+ msg = "Field {0} has bad value '{1}'".format(
+ self._name, field)
log.warning(msg)
return None
@@ -34,8 +42,11 @@ class Date(ModelType):
"""
if value is None:
return None
-
- return time.strftime(self.time_format, value)
+ if isinstance(value, time.struct_time):
+ # struct_times are always utc
+ return time.strftime('%Y-%m-%dT%H:%M:%SZ', value)
+ elif isinstance(value, datetime.datetime):
+ return value.isoformat() + 'Z'
TIMEDELTA_REGEX = re.compile(r'^((?P\d+?) day(?:s?))?(\s)?((?P\d+?) hour(?:s?))?(\s)?((?P\d+?) minute(?:s)?)?(\s)?((?P\d+?) second(?:s)?)?$')
@@ -66,4 +77,4 @@ class Timedelta(ModelType):
cur_value = getattr(value, attr, 0)
if cur_value > 0:
values.append("%d %s" % (cur_value, attr))
- return ' '.join(values)
\ No newline at end of file
+ return ' '.join(values)
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index 1bf4763723..f6fa98fc28 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -8,7 +8,7 @@ from collections import namedtuple
from fs.osfs import OSFS
from itertools import repeat
from path import path
-from datetime import datetime, timedelta
+from datetime import datetime
from importlib import import_module
from xmodule.errortracker import null_error_tracker, exc_info_to_str
@@ -246,6 +246,7 @@ class MongoModuleStore(ModuleStoreBase):
self.fs_root = path(fs_root)
self.error_tracker = error_tracker
self.render_template = render_template
+ self.ignore_write_events_on_courses = []
def get_metadata_inheritance_tree(self, location):
'''
@@ -330,6 +331,11 @@ class MongoModuleStore(ModuleStoreBase):
return tree
+ def refresh_cached_metadata_inheritance_tree(self, location):
+ pseudo_course_id = '/'.join([location.org, location.course])
+ if pseudo_course_id not in self.ignore_write_events_on_courses:
+ self.get_cached_metadata_inheritance_tree(location, force_refresh = True)
+
def clear_cached_metadata_inheritance_tree(self, location):
key_name = '{0}/{1}'.format(location.org, location.course)
if self.metadata_inheritance_cache is not None:
@@ -376,7 +382,7 @@ class MongoModuleStore(ModuleStoreBase):
return data
- def _load_item(self, item, data_cache):
+ def _load_item(self, item, data_cache, should_apply_metadata_inheritence=True):
"""
Load an XModuleDescriptor from item, using the children stored in data_cache
"""
@@ -388,7 +394,10 @@ class MongoModuleStore(ModuleStoreBase):
resource_fs = OSFS(root)
- metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location']))
+ metadata_inheritance_tree = None
+
+ if should_apply_metadata_inheritence:
+ metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location']))
# TODO (cdodge): When the 'split module store' work has been completed, we should remove
# the 'metadata_inheritance_tree' parameter
@@ -410,7 +419,10 @@ class MongoModuleStore(ModuleStoreBase):
"""
data_cache = self._cache_children(items, depth)
- return [self._load_item(item, data_cache) for item in items]
+ # if we are loading a course object, if we're not prefetching children (depth != 0) then don't
+ # bother with the metadata inheritence
+ return [self._load_item(item, data_cache,
+ should_apply_metadata_inheritence=(item['location']['category'] != 'course' or depth != 0)) for item in items]
def get_courses(self):
'''
@@ -493,10 +505,12 @@ class MongoModuleStore(ModuleStoreBase):
try:
source_item = self.collection.find_one(location_to_query(source))
source_item['_id'] = Location(location).dict()
- self.collection.insert(source_item,
+ self.collection.insert(
+ source_item,
# Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False")
# from overriding our default value set in the init method.
- safe=self.collection.safe)
+ safe=self.collection.safe
+ )
item = self._load_items([source_item])[0]
# VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
@@ -518,7 +532,7 @@ class MongoModuleStore(ModuleStoreBase):
raise DuplicateItemError(location)
# recompute (and update) the metadata inheritance tree which is cached
- self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True)
+ self.refresh_cached_metadata_inheritance_tree(Location(location))
def get_course_for_item(self, location, depth=0):
'''
@@ -588,7 +602,7 @@ class MongoModuleStore(ModuleStoreBase):
self._update_single_item(location, {'definition.children': children})
# recompute (and update) the metadata inheritance tree which is cached
- self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True)
+ self.refresh_cached_metadata_inheritance_tree(Location(location))
def update_metadata(self, location, metadata):
"""
@@ -614,7 +628,7 @@ class MongoModuleStore(ModuleStoreBase):
self._update_single_item(location, {'metadata': metadata})
# recompute (and update) the metadata inheritance tree which is cached
- self.get_cached_metadata_inheritance_tree(loc, force_refresh = True)
+ self.refresh_cached_metadata_inheritance_tree(loc)
def delete_item(self, location):
"""
@@ -637,8 +651,7 @@ class MongoModuleStore(ModuleStoreBase):
# from overriding our default value set in the init method.
safe=self.collection.safe)
# recompute (and update) the metadata inheritance tree which is cached
- self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True)
-
+ self.refresh_cached_metadata_inheritance_tree(Location(location))
def get_parent_locations(self, location, course_id):
'''Find all locations that are the parents of this location in this
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py
index b842ffe9dd..1a82e1b708 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py
@@ -25,8 +25,7 @@ class XModuleCourseFactory(Factory):
@classmethod
def _create(cls, target_class, *args, **kwargs):
- # This logic was taken from the create_new_course method in
- # cms/djangoapps/contentstore/views.py
+
template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
org = kwargs.get('org')
number = kwargs.get('number')
@@ -43,8 +42,7 @@ class XModuleCourseFactory(Factory):
if display_name is not None:
new_course.display_name = display_name
- new_course.start = gmtime()
-
+ new_course.lms.start = gmtime()
new_course.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
@@ -81,21 +79,41 @@ class XModuleItemFactory(Factory):
@classmethod
def _create(cls, target_class, *args, **kwargs):
"""
- kwargs must include parent_location, template. Can contain display_name
- target_class is ignored
+ Uses *kwargs*:
+
+ *parent_location* (required): the location of the parent module
+ (e.g. the parent course or section)
+
+ *template* (required): the template to create the item from
+ (e.g. i4x://templates/section/Empty)
+
+ *data* (optional): the data for the item
+ (e.g. XML problem definition for a problem item)
+
+ *display_name* (optional): the display name of the item
+
+ *metadata* (optional): dictionary of metadata attributes
+
+ *target_class* is ignored
"""
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
parent_location = Location(kwargs.get('parent_location'))
template = Location(kwargs.get('template'))
+ data = kwargs.get('data')
display_name = kwargs.get('display_name')
+ metadata = kwargs.get('metadata', {})
store = modulestore('direct')
# This code was based off that in cms/djangoapps/contentstore/views.py
parent = store.get_item(parent_location)
- dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
+
+ # If a display name is set, use that
+ dest_name = display_name.replace(" ", "_") if display_name is not None else uuid4().hex
+ dest_location = parent_location._replace(category=template.category,
+ name=dest_name)
new_item = store.clone_item(template, dest_location)
@@ -103,7 +121,14 @@ class XModuleItemFactory(Factory):
if display_name is not None:
new_item.display_name = display_name
- store.update_metadata(new_item.location.url(), own_metadata(new_item))
+ # Add additional metadata or override current metadata
+ item_metadata = own_metadata(new_item)
+ item_metadata.update(metadata)
+ store.update_metadata(new_item.location.url(), item_metadata)
+
+ # replace the data with the optional *data* parameter
+ if data is not None:
+ store.update_item(new_item.location, data)
if new_item.location.category not in DETACHED_CATEGORIES:
store.update_children(parent_location, parent.children + [new_item.location.url()])
diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
index fa232596f2..6a4ce5131b 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
@@ -4,6 +4,8 @@ import mimetypes
from lxml.html import rewrite_links as lxml_rewrite_links
from path import path
+from xblock.core import Scope
+
from .xml import XMLModuleStore
from .exceptions import DuplicateItemError
from xmodule.modulestore import Location
@@ -201,100 +203,127 @@ def import_from_xml(store, data_dir, course_dirs=None,
course_items = []
for course_id in module_store.modules.keys():
- course_data_path = None
- course_location = None
+ if target_location_namespace is not None:
+ pseudo_course_id = '/'.join([target_location_namespace.org, target_location_namespace.course])
+ else:
+ course_id_components = course_id.split('/')
+ pseudo_course_id = '/'.join([course_id_components[0], course_id_components[1]])
- if verbose:
- log.debug("Scanning {0} for course module...".format(course_id))
+ try:
+ # turn off all write signalling while importing as this is a high volume operation
+ if pseudo_course_id not in store.ignore_write_events_on_courses:
+ store.ignore_write_events_on_courses.append(pseudo_course_id)
- # Quick scan to get course module as we need some info from there. Also we need to make sure that the
- # course module is committed first into the store
- for module in module_store.modules[course_id].itervalues():
- if module.category == 'course':
- course_data_path = path(data_dir) / module.data_dir
- course_location = module.location
-
- module = remap_namespace(module, target_location_namespace)
-
- # cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
- # does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
- # but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
- # if there is *any* tabs - then there at least needs to be some predefined ones
- if module.tabs is None or len(module.tabs) == 0:
- module.tabs = [{"type": "courseware"},
- {"type": "course_info", "name": "Course Info"},
- {"type": "discussion", "name": "Discussion"},
- {"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
-
-
- if hasattr(module, 'data'):
- store.update_item(module.location, module.data)
- store.update_children(module.location, module.children)
- store.update_metadata(module.location, dict(own_metadata(module)))
-
- # a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg
- # so let's make sure we import in case there are no other references to it in the modules
- verify_content_links(module, course_data_path, static_content_store, '/static/images/course_image.jpg')
-
- course_items.append(module)
-
-
- # then import all the static content
- if static_content_store is not None:
- _namespace_rename = target_location_namespace if target_location_namespace is not None else course_location
-
- # first pass to find everything in /static/
- import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store,
- _namespace_rename, subpath='static', verbose=verbose)
-
- # finally loop through all the modules
- for module in module_store.modules[course_id].itervalues():
-
- if module.category == 'course':
- # we've already saved the course module up at the top of the loop
- # so just skip over it in the inner loop
- continue
-
- # remap module to the new namespace
- if target_location_namespace is not None:
- module = remap_namespace(module, target_location_namespace)
+ course_data_path = None
+ course_location = None
if verbose:
- log.debug('importing module location {0}'.format(module.location))
+ log.debug("Scanning {0} for course module...".format(course_id))
- if hasattr(module, 'data'):
- module_data = module.data
+ # Quick scan to get course module as we need some info from there. Also we need to make sure that the
+ # course module is committed first into the store
+ for module in module_store.modules[course_id].itervalues():
+ if module.category == 'course':
+ course_data_path = path(data_dir) / module.data_dir
+ course_location = module.location
- # cdodge: now go through any link references to '/static/' and make sure we've imported
- # it as a StaticContent asset
- try:
- remap_dict = {}
+ module = remap_namespace(module, target_location_namespace)
- # use the rewrite_links as a utility means to enumerate through all links
- # in the module data. We use that to load that reference into our asset store
- # IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to
- # do the rewrites natively in that code.
- # For example, what I'm seeing is ->
- # Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
- # no good, so we have to do this kludge
- if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
- lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path,
- static_content_store, link, remap_dict))
+ # cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
+ # does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
+ # but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
+ # if there is *any* tabs - then there at least needs to be some predefined ones
+ if module.tabs is None or len(module.tabs) == 0:
+ module.tabs = [{"type": "courseware"},
+ {"type": "course_info", "name": "Course Info"},
+ {"type": "discussion", "name": "Discussion"},
+ {"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
- for key in remap_dict.keys():
- module_data = module_data.replace(key, remap_dict[key])
- except Exception, e:
- logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
+ if hasattr(module, 'data'):
+ store.update_item(module.location, module.data)
+ store.update_children(module.location, module.children)
+ store.update_metadata(module.location, dict(own_metadata(module)))
- store.update_item(module.location, module_data)
+ # a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg
+ # so let's make sure we import in case there are no other references to it in the modules
+ verify_content_links(module, course_data_path, static_content_store, '/static/images/course_image.jpg')
- if hasattr(module, 'children') and module.children != []:
- store.update_children(module.location, module.children)
+ course_items.append(module)
- # NOTE: It's important to use own_metadata here to avoid writing
- # inherited metadata everywhere.
- store.update_metadata(module.location, dict(own_metadata(module)))
+
+ # then import all the static content
+ if static_content_store is not None:
+ _namespace_rename = target_location_namespace if target_location_namespace is not None else course_location
+
+ # first pass to find everything in /static/
+ import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store,
+ _namespace_rename, subpath='static', verbose=verbose)
+
+ # finally loop through all the modules
+ for module in module_store.modules[course_id].itervalues():
+
+ if module.category == 'course':
+ # we've already saved the course module up at the top of the loop
+ # so just skip over it in the inner loop
+ continue
+
+ # remap module to the new namespace
+ if target_location_namespace is not None:
+ module = remap_namespace(module, target_location_namespace)
+
+ if verbose:
+ log.debug('importing module location {0}'.format(module.location))
+
+ content = {}
+ for field in module.fields:
+ if field.scope != Scope.content:
+ continue
+ try:
+ content[field.name] = module._model_data[field.name]
+ except KeyError:
+ # Ignore any missing keys in _model_data
+ pass
+
+ if 'data' in content:
+ module_data = content['data']
+
+ # cdodge: now go through any link references to '/static/' and make sure we've imported
+ # it as a StaticContent asset
+ try:
+ remap_dict = {}
+
+ # use the rewrite_links as a utility means to enumerate through all links
+ # in the module data. We use that to load that reference into our asset store
+ # IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to
+ # do the rewrites natively in that code.
+ # For example, what I'm seeing is ->
+ # Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
+ # no good, so we have to do this kludge
+ if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
+ lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path,
+ static_content_store, link, remap_dict))
+
+ for key in remap_dict.keys():
+ module_data = module_data.replace(key, remap_dict[key])
+
+ except Exception, e:
+ logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
+
+ store.update_item(module.location, content)
+
+ if hasattr(module, 'children') and module.children != []:
+ store.update_children(module.location, module.children)
+
+ # NOTE: It's important to use own_metadata here to avoid writing
+ # inherited metadata everywhere.
+ store.update_metadata(module.location, dict(own_metadata(module)))
+ finally:
+ # turn back on all write signalling
+ if pseudo_course_id in store.ignore_write_events_on_courses:
+ store.ignore_write_events_on_courses.remove(pseudo_course_id)
+ store.refresh_cached_metadata_inheritance_tree(target_location_namespace if
+ target_location_namespace is not None else course_location)
return module_store, course_items
diff --git a/common/lib/xmodule/xmodule/templates/course/empty.yaml b/common/lib/xmodule/xmodule/templates/course/empty.yaml
index cb2f3bcec6..7b25c21ad6 100644
--- a/common/lib/xmodule/xmodule/templates/course/empty.yaml
+++ b/common/lib/xmodule/xmodule/templates/course/empty.yaml
@@ -2,5 +2,123 @@
metadata:
display_name: Empty
start: 2020-10-10T10:00
+ checklists: [
+ {"short_description" : "Getting Started With Studio",
+ "items" : [{"short_description": "Add Course Team Members",
+ "long_description": "Grant your collaborators permission to edit your course so you can work together.",
+ "is_checked": false,
+ "action_url": "ManageUsers",
+ "action_text": "Edit Course Team",
+ "action_external": false},
+ {"short_description": "Set Important Dates for Your Course",
+ "long_description": "Establish your course's student enrollment and launch dates on the Schedule and Details page.",
+ "is_checked": false,
+ "action_url": "SettingsDetails",
+ "action_text": "Edit Course Details & Schedule",
+ "action_external": false},
+ {"short_description": "Draft Your Course's Grading Policy",
+ "long_description": "Set up your assignment types and grading policy even if you haven't created all your assignments.",
+ "is_checked": false,
+ "action_url": "SettingsGrading",
+ "action_text": "Edit Grading Settings",
+ "action_external": false},
+ {"short_description": "Explore the Other Studio Checklists",
+ "long_description": "Discover other available course authoring tools, and find help when you need it.",
+ "is_checked": false,
+ "action_url": "",
+ "action_text": "",
+ "action_external": false}]
+ },
+ {"short_description" : "Draft a Rough Course Outline",
+ "items" : [{"short_description": "Create Your First Section and Subsection",
+ "long_description": "Use your course outline to build your first Section and Subsection.",
+ "is_checked": false,
+ "action_url": "CourseOutline",
+ "action_text": "Edit Course Outline",
+ "action_external": false},
+ {"short_description": "Set Section Release Dates",
+ "long_description": "Specify the release dates for each Section in your course. Sections become visible to students on their release dates.",
+ "is_checked": false,
+ "action_url": "CourseOutline",
+ "action_text": "Edit Course Outline",
+ "action_external": false},
+ {"short_description": "Designate a Subsection as Graded",
+ "long_description": "Set a Subsection to be graded as a specific assignment type. Assignments within graded Subsections count toward a student's final grade.",
+ "is_checked": false,
+ "action_url": "CourseOutline",
+ "action_text": "Edit Course Outline",
+ "action_external": false},
+ {"short_description": "Reordering Course Content",
+ "long_description": "Use drag and drop to reorder the content in your course.",
+ "is_checked": false,
+ "action_url": "CourseOutline",
+ "action_text": "Edit Course Outline",
+ "action_external": false},
+ {"short_description": "Renaming Sections",
+ "long_description": "Rename Sections by clicking the Section name from the Course Outline.",
+ "is_checked": false,
+ "action_url": "CourseOutline",
+ "action_text": "Edit Course Outline",
+ "action_external": false},
+ {"short_description": "Deleting Course Content",
+ "long_description": "Delete Sections, Subsections, or Units you don't need anymore. Be careful, as there is no Undo function.",
+ "is_checked": false,
+ "action_url": "CourseOutline",
+ "action_text": "Edit Course Outline",
+ "action_external": false},
+ {"short_description": "Add an Instructor-Only Section to Your Outline",
+ "long_description": "Some course authors find using a section for unsorted, in-progress work useful. To do this, create a section and set the release date to the distant future.",
+ "is_checked": false,
+ "action_url": "CourseOutline",
+ "action_text": "Edit Course Outline",
+ "action_external": false}]
+ },
+ {"short_description" : "Explore edX's Support Tools",
+ "items" : [{"short_description": "Explore the Studio Help Forum",
+ "long_description": "Access the Studio Help forum from the menu that appears when you click your user name in the top right corner of Studio.",
+ "is_checked": false,
+ "action_url": "http://help.edge.edx.org/",
+ "action_text": "Visit Studio Help",
+ "action_external": true},
+ {"short_description": "Enroll in edX 101",
+ "long_description": "Register for edX 101, edX's primer for course creation.",
+ "is_checked": false,
+ "action_url": "https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about",
+ "action_text": "Register for edX 101",
+ "action_external": true},
+ {"short_description": "Download the Studio Documentation",
+ "long_description": "Download the searchable Studio reference documentation in PDF form.",
+ "is_checked": false,
+ "action_url": "http://help.edge.edx.org/help/assets/8ccd409f979c8639dd463e126eb840dc67f09098/Getting_Started_with_Studio.pdf",
+ "action_text": "Download Documentation",
+ "action_external": true}]
+ },
+ {"short_description" : "Draft Your Course About Page",
+ "items" : [{"short_description": "Draft a Course Description",
+ "long_description": "Courses on edX have an About page that includes a course video, description, and more. Draft the text students will read before deciding to enroll in your course.",
+ "is_checked": false,
+ "action_url": "SettingsDetails",
+ "action_text": "Edit Course Schedule & Details",
+ "action_external": false},
+ {"short_description": "Add Staff Bios",
+ "long_description": "Showing prospective students who their instructor will be is helpful. Include staff bios on the course About page.",
+ "is_checked": false,
+ "action_url": "SettingsDetails",
+ "action_text": "Edit Course Schedule & Details",
+ "action_external": false},
+ {"short_description": "Add Course FAQs",
+ "long_description": "Include a short list of frequently asked questions about your course.",
+ "is_checked": false,
+ "action_url": "SettingsDetails",
+ "action_text": "Edit Course Schedule & Details",
+ "action_external": false},
+ {"short_description": "Add Course Prerequisites",
+ "long_description": "Let students know what knowledge and/or skills they should have before they enroll in your course.",
+ "is_checked": false,
+ "action_url": "SettingsDetails",
+ "action_text": "Edit Course Schedule & Details",
+ "action_external": false}]
+ }
+ ]
data: { 'textbooks' : [ ], 'wiki_slug' : null }
children: []
diff --git a/common/static/js/vendor/jquery.smooth-scroll.min.js b/common/static/js/vendor/jquery.smooth-scroll.min.js
new file mode 100755
index 0000000000..2af596ee83
--- /dev/null
+++ b/common/static/js/vendor/jquery.smooth-scroll.min.js
@@ -0,0 +1,7 @@
+/*!
+ * Smooth Scroll - v1.4.10 - 2013-03-02
+ * https://github.com/kswedberg/jquery-smooth-scroll
+ * Copyright (c) 2013 Karl Swedberg
+ * Licensed MIT (https://github.com/kswedberg/jquery-smooth-scroll/blob/master/LICENSE-MIT)
+ */
+(function(l){function t(l){return l.replace(/(:|\.)/g,"\\$1")}var e="1.4.10",o={exclude:[],excludeWithin:[],offset:0,direction:"top",scrollElement:null,scrollTarget:null,beforeScroll:function(){},afterScroll:function(){},easing:"swing",speed:400,autoCoefficent:2},r=function(t){var e=[],o=!1,r=t.dir&&"left"==t.dir?"scrollLeft":"scrollTop";return this.each(function(){if(this!=document&&this!=window){var t=l(this);t[r]()>0?e.push(this):(t[r](1),o=t[r]()>0,o&&e.push(this),t[r](0))}}),e.length||this.each(function(){"BODY"===this.nodeName&&(e=[this])}),"first"===t.el&&e.length>1&&(e=[e[0]]),e};l.fn.extend({scrollable:function(l){var t=r.call(this,{dir:l});return this.pushStack(t)},firstScrollable:function(l){var t=r.call(this,{el:"first",dir:l});return this.pushStack(t)},smoothScroll:function(e){e=e||{};var o=l.extend({},l.fn.smoothScroll.defaults,e),r=l.smoothScroll.filterPath(location.pathname);return this.unbind("click.smoothscroll").bind("click.smoothscroll",function(e){var n=this,s=l(this),c=o.exclude,i=o.excludeWithin,a=0,f=0,h=!0,u={},d=location.hostname===n.hostname||!n.hostname,m=o.scrollTarget||(l.smoothScroll.filterPath(n.pathname)||r)===r,p=t(n.hash);if(o.scrollTarget||d&&m&&p){for(;h&&c.length>a;)s.is(t(c[a++]))&&(h=!1);for(;h&&i.length>f;)s.closest(i[f++]).length&&(h=!1)}else h=!1;h&&(e.preventDefault(),l.extend(u,o,{scrollTarget:o.scrollTarget||p,link:n}),l.smoothScroll(u))}),this}}),l.smoothScroll=function(t,e){var o,r,n,s,c=0,i="offset",a="scrollTop",f={},h={};"number"==typeof t?(o=l.fn.smoothScroll.defaults,n=t):(o=l.extend({},l.fn.smoothScroll.defaults,t||{}),o.scrollElement&&(i="position","static"==o.scrollElement.css("position")&&o.scrollElement.css("position","relative"))),o=l.extend({link:null},o),a="left"==o.direction?"scrollLeft":a,o.scrollElement?(r=o.scrollElement,c=r[a]()):r=l("html, body").firstScrollable(),o.beforeScroll.call(r,o),n="number"==typeof t?t:e||l(o.scrollTarget)[i]()&&l(o.scrollTarget)[i]()[o.direction]||0,f[a]=n+c+o.offset,s=o.speed,"auto"===s&&(s=f[a]||r.scrollTop(),s/=o.autoCoefficent),h={duration:s,easing:o.easing,complete:function(){o.afterScroll.call(o.link,o)}},o.step&&(h.step=o.step),r.length?r.stop().animate(f,h):o.afterScroll.call(o.link,o)},l.smoothScroll.version=e,l.smoothScroll.filterPath=function(l){return l.replace(/^\//,"").replace(/(index|default).[a-zA-Z]{3,4}$/,"").replace(/\/$/,"")},l.fn.smoothScroll.defaults=o})(jQuery);
\ No newline at end of file
diff --git a/common/static/sass/_mixins.scss b/common/static/sass/_mixins.scss
index 76d52ed930..c1dd5b7f2d 100644
--- a/common/static/sass/_mixins.scss
+++ b/common/static/sass/_mixins.scss
@@ -1,9 +1,12 @@
+// studio - utilities - mixins and extends
+// ====================
+
// font-sizing
@function em($pxval, $base: 16) {
@return #{$pxval / $base}em;
}
-@mixin font-size($sizeValue: 1.6){
+@mixin font-size($sizeValue: 16){
font-size: $sizeValue + px;
font-size: ($sizeValue/10) + rem;
}
@@ -64,4 +67,106 @@
:-ms-input-placeholder {
color: $color;
}
+}
+
+// ====================
+
+// extends - visual
+.faded-hr-divider {
+ @include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
+ rgba(200,200,200, 1) 50%,
+ rgba(200,200,200, 0)));
+ height: 1px;
+ width: 100%;
+}
+
+.faded-hr-divider-medium {
+ @include background-image(linear-gradient(180deg, rgba(240,240,240, 0) 0%,
+ rgba(240,240,240, 1) 50%,
+ rgba(240,240,240, 0)));
+ height: 1px;
+ width: 100%;
+}
+
+.faded-hr-divider-light {
+ @include background-image(linear-gradient(180deg, rgba(255,255,255, 0) 0%,
+ rgba(255,255,255, 0.8) 50%,
+ rgba(255,255,255, 0)));
+ height: 1px;
+ width: 100%;
+}
+
+.faded-vertical-divider {
+ @include background-image(linear-gradient(90deg, rgba(200,200,200, 0) 0%,
+ rgba(200,200,200, 1) 50%,
+ rgba(200,200,200, 0)));
+ height: 100%;
+ width: 1px;
+}
+
+.faded-vertical-divider-light {
+ @include background-image(linear-gradient(90deg, rgba(255,255,255, 0) 0%,
+ rgba(255,255,255, 0.6) 50%,
+ rgba(255,255,255, 0)));
+ height: 100%;
+ width: 1px;
+}
+
+.vertical-divider {
+ @extend .faded-vertical-divider;
+ position: relative;
+
+ &::after {
+ @extend .faded-vertical-divider-light;
+ content: "";
+ display: block;
+ position: absolute;
+ left: 1px;
+ }
+}
+
+.horizontal-divider {
+ border: none;
+ @extend .faded-hr-divider;
+ position: relative;
+
+ &::after {
+ @extend .faded-hr-divider-light;
+ content: "";
+ display: block;
+ position: absolute;
+ top: 1px;
+ }
+}
+
+.fade-right-hr-divider {
+ @include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
+ rgba(200,200,200, 1)));
+ border: none;
+}
+
+.fade-left-hr-divider {
+ @include background-image(linear-gradient(180deg, rgba(200,200,200, 1) 0%,
+ rgba(200,200,200, 0)));
+ border: none;
+}
+
+// extends - ui
+.window {
+ @include clearfix();
+ @include border-radius(3px);
+ @include box-shadow(0 1px 1px $shadow-l1);
+ margin-bottom: $baseline;
+ border: 1px solid $gray-l2;
+ background: $white;
+}
+
+.elem-d1 {
+ @include clearfix();
+ @include box-sizing(border-box);
+}
+
+.elem-d2 {
+ @include clearfix();
+ @include box-sizing(border-box);
}
\ No newline at end of file
diff --git a/common/test/data/full/vertical/vertical_89.xml b/common/test/data/full/vertical/vertical_89.xml
index c2b68b6bc2..cf2dd23462 100644
--- a/common/test/data/full/vertical/vertical_89.xml
+++ b/common/test/data/full/vertical/vertical_89.xml
@@ -7,4 +7,9 @@
+
+
Have you changed your mind?
+ Yes
+ No
+
diff --git a/doc/public/Makefile b/doc/public/Makefile
index f162e1b05c..378a64b7fb 100644
--- a/doc/public/Makefile
+++ b/doc/public/Makefile
@@ -5,7 +5,7 @@
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
-BUILDDIR = _build
+BUILDDIR = build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
diff --git a/doc/public/course_data_formats/conditional_module/conditional_module.rst b/doc/public/course_data_formats/conditional_module/conditional_module.rst
new file mode 100644
index 0000000000..82c555d3e7
--- /dev/null
+++ b/doc/public/course_data_formats/conditional_module/conditional_module.rst
@@ -0,0 +1,77 @@
+**********************************************
+Xml format of conditional module [xmodule]
+**********************************************
+
+.. module:: conditional_module
+
+Format description
+==================
+
+The main tag of Conditional module input is:
+
+.. code-block:: xml
+
+ ...
+
+``conditional`` can include any number of any xmodule tags (``html``, ``video``, ``poll``, etc.) or ``show`` tags.
+
+conditional tag
+---------------
+
+The main container for a single instance of Conditional module. The following attributes can
+be specified for this tag::
+
+ sources - location id of required modules, separated by ';'
+ [message | ""] - message for case, where one or more are not passed. Here you can use variable {link}, which generate link to required module.
+
+ [completed] - map to `is_completed` module method
+ [attempted] - map to `is_attempted` module method
+ [poll_answer] - map to `poll_answer` module attribute
+ [voted] - map to `voted` module attribute
+
+show tag
+--------
+
+Symlink to some set of xmodules. The following attributes can
+be specified for this tag::
+
+ sources - location id of modules, separated by ';'
+
+Example
+=======
+
+Examples of conditional depends on poll
+-------------------------------------------
+
+.. code-block:: xml
+
+
+
+
You see this, cause your vote value for "First question" was "man"
+
+
+
+Examples of conditional depends on poll (use tag)
+-------------------------------------------
+
+.. code-block:: xml
+
+
+
+
+
+
+
+Examples of conditional depends on problem
+-------------------------------------------
+
+.. code-block:: xml
+
+
+ You see this, cause "lec27_Q1" is attempted.
+
+
+ You see this, cause "lec27_Q1" is not attempted.
+
\ No newline at end of file
diff --git a/doc/public/course_data_formats/poll_module/poll_module.rst b/doc/public/course_data_formats/poll_module/poll_module.rst
new file mode 100644
index 0000000000..9b16758877
--- /dev/null
+++ b/doc/public/course_data_formats/poll_module/poll_module.rst
@@ -0,0 +1,67 @@
+**********************************************
+Xml format of poll module [xmodule]
+**********************************************
+
+.. module:: poll_module
+
+Format description
+==================
+
+The main tag of Poll module input is:
+
+.. code-block:: xml
+
+ ...
+
+``poll_question`` can include any number of the following tags:
+any xml and ``answer`` tag. All inner xml, except for ``answer`` tags, we call "question".
+
+poll_question tag
+-----------------
+
+Xmodule for creating poll functionality - voting system. The following attributes can
+be specified for this tag::
+
+ name - Name of xmodule.
+ [display_name| AUTOGENERATE] - Display name of xmodule. When this attribute is not defined - display name autogenerate with some hash.
+ [reset | False] - Can reset/revote many time (value = True/False)
+
+
+answer tag
+----------
+
+Define one of the possible answer for poll module. The following attributes can
+be specified for this tag::
+
+ id - unique identifier (using to identify the different answers)
+
+Inner text - Display text for answer choice.
+
+Example
+=======
+
+Examples of poll
+----------------
+
+.. code-block:: xml
+
+
+
Age
+
How old are you?
+ < 18
+ from 10 to 25
+ > 25
+
+
+Examples of poll with unable reset functionality
+------------------------------------------------
+
+.. code-block:: xml
+
+
+
Your gender
+
You are man or woman?
+ Man
+ Woman
+
\ No newline at end of file
diff --git a/doc/public/index.rst b/doc/public/index.rst
index 084340e855..ee681a822e 100644
--- a/doc/public/index.rst
+++ b/doc/public/index.rst
@@ -24,6 +24,8 @@ Specific Problem Types
course_data_formats/drag_and_drop/drag_and_drop_input.rst
course_data_formats/graphical_slider_tool/graphical_slider_tool.rst
+ course_data_formats/poll_module/poll_module.rst
+ course_data_formats/conditional_module/conditional_module.rst
course_data_formats/custom_response.rst
diff --git a/doc/public/internal_data_formats/sql_schema.rst b/doc/public/internal_data_formats/sql_schema.rst
index 409ec1c065..92c5c4fa0e 100644
--- a/doc/public/internal_data_formats/sql_schema.rst
+++ b/doc/public/internal_data_formats/sql_schema.rst
@@ -313,14 +313,18 @@ There is an important split in demographic data gathered for the students who si
- This student signed up before this information was collected
* - `''` (blank)
- User did not specify level of education.
+ * - `'p'`
+ - Doctorate
* - `'p_se'`
- - Doctorate in science or engineering
+ - Doctorate in science or engineering (no longer used)
* - `'p_oth'`
- - Doctorate in another field
+ - Doctorate in another field (no longer used)
* - `'m'`
- Master's or professional degree
* - `'b'`
- Bachelor's degree
+ * - `'a'`
+ - Associate's degree
* - `'hs'`
- Secondary/high school
* - `'jhs'`
@@ -624,4 +628,4 @@ The generatedcertificate table tracks certificate state for students who have be
`grade`
-------
- The grade of the student recorded at the time the certificate was generated. This may be different than the current grade since grading is only done once for a course when it ends.
\ No newline at end of file
+ The grade of the student recorded at the time the certificate was generated. This may be different than the current grade since grading is only done once for a course when it ends.
diff --git a/docs/source/drag-n-drop-demo.xml b/docs/source/drag-n-drop-demo.xml
deleted file mode 100644
index 67712407a1..0000000000
--- a/docs/source/drag-n-drop-demo.xml
+++ /dev/null
@@ -1,526 +0,0 @@
-
-
-
-
-
[Anyof rule example]
-
Please label hydrogen atoms connected with left carbon atom.
[Exact number of draggables for a set of targets.]
-
Drag two Grass and one Star to first or second positions, and three Cloud to any of the three positions.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
[As many as you like draggables for a set of targets.]
-
Drag some Grass to any of the targets, and some Stars to either first or last target.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/source/drag_and_drop_input.rst b/docs/source/drag_and_drop_input.rst
deleted file mode 100644
index 06a28a5926..0000000000
--- a/docs/source/drag_and_drop_input.rst
+++ /dev/null
@@ -1,323 +0,0 @@
-**********************************************
-Xml format of drag and drop input [inputtypes]
-**********************************************
-
-.. module:: drag_and_drop_input
-
-Format description
-==================
-
-The main tag of Drag and Drop (DnD) input is::
-
- ...
-
-``drag_and_drop_input`` can include any number of the following 2 tags:
-``draggable`` and ``target``.
-
-drag_and_drop_input tag
------------------------
-
-The main container for a single instance of DnD. The following attributes can
-be specified for this tag::
-
- img - Relative path to an image that will be the base image. All draggables
- can be dragged onto it.
- target_outline - Specify whether an outline (gray dashed line) should be
- drawn around targets (if they are specified). It can be either
- 'true' or 'false'. If not specified, the default value is
- 'false'.
- one_per_target - Specify whether to allow more than one draggable to be
- placed onto a single target. It can be either 'true' or 'false'. If
- not specified, the default value is 'true'.
- no_labels - default is false, in default behaviour if label is not set, label
- is obtained from id. If no_labels is true, labels are not automatically
- populated from id, and one can not set labels and obtain only icons.
-
-draggable tag
--------------
-
-Draggable tag specifies a single draggable object which has the following
-attributes::
-
- id - Unique identifier of the draggable object.
- label - Human readable label that will be shown to the user.
- icon - Relative path to an image that will be shown to the user.
- can_reuse - true or false, default is false. If true, same draggable can be
- used multiple times.
-
-A draggable is what the user must drag out of the slider and place onto the
-base image. After a drag operation, if the center of the draggable ends up
-outside the rectangular dimensions of the image, it will be returned back
-to the slider.
-
-In order for the grader to work, it is essential that a unique ID
-is provided. Otherwise, there will be no way to tell which draggable is at what
-coordinate, or over what target. Label and icon attributes are optional. If
-they are provided they will be used, otherwise, you can have an empty
-draggable. The path is relative to 'course_folder' folder, for example,
-/static/images/img1.png.
-
-target tag
-----------
-
-Target tag specifies a single target object which has the following required
-attributes::
-
- id - Unique identifier of the target object.
- x - X-coordinate on the base image where the top left corner of the target
- will be positioned.
- y - Y-coordinate on the base image where the top left corner of the target
- will be positioned.
- w - Width of the target.
- h - Height of the target.
-
-A target specifies a place on the base image where a draggable can be
-positioned. By design, if the center of a draggable lies within the target
-(i.e. in the rectangle defined by [[x, y], [x + w, y + h]], then it is within
-the target. Otherwise, it is outside.
-
-If at lest one target is provided, the behavior of the client side logic
-changes. If a draggable is not dragged on to a target, it is returned back to
-the slider.
-
-If no targets are provided, then a draggable can be dragged and placed anywhere
-on the base image.
-
-correct answer format
----------------------
-
-There are two correct answer formats: short and long
-If short from correct answer is mapping of 'draggable_id' to 'target_id'::
-
- correct_answer = {'grass': [[300, 200], 200], 'ant': [[500, 0], 200]}
- correct_answer = {'name4': 't1', '7': 't2'}
-
-In long form correct answer is list of dicts. Every dict has 3 keys:
-draggables, targets and rule. For example::
-
- correct_answer = [
- {
- 'draggables': ['7', '8'],
- 'targets': ['t5_c', 't6_c'],
- 'rule': 'anyof'
- },
- {
- 'draggables': ['1', '2'],
- 'targets': ['t2_h', 't3_h', 't4_h', 't7_h', 't8_h', 't10_h'],
- 'rule': 'anyof'
- }]
-
-Draggables is list of draggables id. Target is list of targets id, draggables
-must be dragged to with considering rule. Rule is string.
-
-Draggables in dicts inside correct_answer list must not intersect!!!
-
-Wrong (for draggable id 7)::
-
- correct_answer = [
- {
- 'draggables': ['7', '8'],
- 'targets': ['t5_c', 't6_c'],
- 'rule': 'anyof'
- },
- {
- 'draggables': ['7', '2'],
- 'targets': ['t2_h', 't3_h', 't4_h', 't7_h', 't8_h', 't10_h'],
- 'rule': 'anyof'
- }]
-
-Rules are: exact, anyof, unordered_equal, anyof+number, unordered_equal+number
-
-
-.. such long lines are needed for sphinx to display lists correctly
-
-- Exact rule means that targets for draggable id's in user_answer are the same that targets from correct answer. For example, for draggables 7 and 8 user must drag 7 to target1 and 8 to target2 if correct_answer is::
-
- correct_answer = [
- {
- 'draggables': ['7', '8'],
- 'targets': ['tartget1', 'target2'],
- 'rule': 'exact'
- }]
-
-
-- unordered_equal rule allows draggables be dragged to targets unordered. If one want to allow for student to drag 7 to target1 or target2 and 8 to target2 or target 1 and 7 and 8 must be in different targets, then correct answer must be::
-
- correct_answer = [
- {
- 'draggables': ['7', '8'],
- 'targets': ['tartget1', 'target2'],
- 'rule': 'unordered_equal'
- }]
-
-
-- Anyof rule allows draggables to be dragged to any of targets. If one want to allow for student to drag 7 and 8 to target1 or target2, which means that if 7 is on target1 and 8 is on target1 or 7 on target2 and 8 on target2 or 7 on target1 and 8 on target2. Any of theese are correct which anyof rule::
-
- correct_answer = [
- {
- 'draggables': ['7', '8'],
- 'targets': ['tartget1', 'target2'],
- 'rule': 'anyof'
- }]
-
-
-- If you have can_reuse true, then you, for example, have draggables a,b,c and 10 targets. These will allow you to drag 4 'a' draggables to ['target1', 'target4', 'target7', 'target10'] , you do not need to write 'a' four times. Also this will allow you to drag 'b' draggable to target2 or target5 for target5 and target2 etc..::
-
- correct_answer = [
- {
- 'draggables': ['a'],
- 'targets': ['target1', 'target4', 'target7', 'target10'],
- 'rule': 'unordered_equal'
- },
- {
- 'draggables': ['b'],
- 'targets': ['target2', 'target5', 'target8'],
- 'rule': 'anyof'
- },
- {
- 'draggables': ['c'],
- 'targets': ['target3', 'target6', 'target9'],
- 'rule': 'unordered_equal'
- }]
-
-- And sometimes you want to allow drag only two 'b' draggables, in these case you sould use 'anyof+number' of 'unordered_equal+number' rule::
-
- correct_answer = [
- {
- 'draggables': ['a', 'a', 'a'],
- 'targets': ['target1', 'target4', 'target7'],
- 'rule': 'unordered_equal+numbers'
- },
- {
- 'draggables': ['b', 'b'],
- 'targets': ['target2', 'target5', 'target8'],
- 'rule': 'anyof+numbers'
- },
- {
- 'draggables': ['c'],
- 'targets': ['target3', 'target6', 'target9'],
- 'rule': 'unordered_equal'
- }]
-
-In case if we have no multiple draggables per targets (one_per_target="true"),
-for same number of draggables, anyof is equal to unordered_equal
-
-If we have can_reuse=true, than one must use only long form of correct answer.
-
-
-Grading logic
--------------
-
-1. User answer (that comes from browser) and correct answer (from xml) are parsed to the same format::
-
- group_id: group_draggables, group_targets, group_rule
-
-
-Group_id is ordinal number, for every dict in correct answer incremental
-group_id is assigned: 0, 1, 2, ...
-
-Draggables from user answer are added to same group_id where identical draggables
-from correct answer are, for example::
-
- If correct_draggables[group_0] = [t1, t2] then
- user_draggables[group_0] are all draggables t1 and t2 from user answer:
- [t1] or [t1, t2] or [t1, t2, t2] etc..
-
-2. For every group from user answer, for that group draggables, if 'number' is in group rule, set() is applied,
-if 'number' is not in rule, set is not applied::
-
- set() : [t1, t2, t3, t3] -> [t1, t2, ,t3]
-
-For every group, at this step, draggables lists are equal.
-
-
-3. For every group, lists of targets are compared using rule for that group.
-
-
-Set and '+number' cases
-.......................
-
-Set() and '+number' are needed only for case of reusable draggables,
-for other cases there are no equal draggables in list, so set() does nothing.
-
-.. such long lines needed for sphinx to display nicely
-
-* Usage of set() operation allows easily create rule for case of "any number of same draggable can be dragged to some targets"::
-
- {
- 'draggables': ['draggable_1'],
- 'targets': ['target3', 'target6', 'target9'],
- 'rule': 'anyof'
- }
-
-
-
-
-* 'number' rule is used for the case of reusable draggables, when one want to fix number of draggable to drag. In this example only two instances of draggables_1 are allowed to be dragged::
-
- {
- 'draggables': ['draggable_1', 'draggable_1'],
- 'targets': ['target3', 'target6', 'target9'],
- 'rule': 'anyof+number'
- }
-
-
-* Note, that in using rule 'exact', one does not need 'number', because you can't recognize from user interface which reusable draggable is on which target. Absurd example::
-
- {
- 'draggables': ['draggable_1', 'draggable_1', 'draggable_2'],
- 'targets': ['target3', 'target6', 'target9'],
- 'rule': 'exact'
- }
-
-
- Correct handling of this example is to create different rules for draggable_1 and
- draggable_2
-
-* For 'unordered_equal' (or 'exact' too) we don't need 'number' if you have only same draggable in group, as targets length will provide constraint for the number of draggables::
-
- {
- 'draggables': ['draggable_1'],
- 'targets': ['target3', 'target6', 'target9'],
- 'rule': 'unordered_equal'
- }
-
-
- This means that only three draggaggables 'draggable_1' can be dragged.
-
-* But if you have more that one different reusable draggable in list, you may use 'number' rule::
-
- {
- 'draggables': ['draggable_1', 'draggable_1', 'draggable_2'],
- 'targets': ['target3', 'target6', 'target9'],
- 'rule': 'unordered_equal+number'
- }
-
-
- If not use number, draggables list will be setted to ['draggable_1', 'draggable_2']
-
-
-
-
-Logic flow
-----------
-
-(Click on image to see full size version.)
-
-.. image:: draganddrop_logic_flow.png
- :width: 100%
- :target: _images/draganddrop_logic_flow.png
-
-
-Example
-=======
-
-Examples of draggables that can't be reused
--------------------------------------------
-
-.. literalinclude:: drag-n-drop-demo.xml
-
-Draggables can be reused
-------------------------
-
-.. literalinclude:: drag-n-drop-demo2.xml
diff --git a/docs/source/draganddrop_logic_flow.png b/docs/source/draganddrop_logic_flow.png
deleted file mode 100644
index 2bb1c11a41..0000000000
Binary files a/docs/source/draganddrop_logic_flow.png and /dev/null differ
diff --git a/docs/source/graphical_slider_tool.rst b/docs/source/graphical_slider_tool.rst
deleted file mode 100644
index 37b17136e8..0000000000
--- a/docs/source/graphical_slider_tool.rst
+++ /dev/null
@@ -1,563 +0,0 @@
-*********************************************
-Xml format of graphical slider tool [xmodule]
-*********************************************
-
-.. module:: xml_format_gst
-
-
-Format description
-==================
-
-Graphical slider tool (GST) main tag is::
-
- BODY
-
-``graphical_slider_tool`` tag must have two children tags: ``render``
-and ``configuration``.
-
-
-Render tag
-----------
-
-Render tag can contain usual html tags mixed with some GST specific tags::
-
- - represents jQuery slider for changing a parameter's value
- - represents a text input field for changing a parameter's value
- - represents Flot JS plot element
-
-Also GST will track all elements inside ```` where ``id``
-attribute is set, and a corresponding parameter referencing that ``id`` is present
-in the configuration section below. These will be referred to as dynamic elements.
-
-The contents of the section will be shown to the user after
-all occurrences of::
-
-
-
-
-
-have been converted to actual sliders, text inputs, and a plot graph.
-Everything in square brackets is optional. After initialization, all
-text input fields, sliders, and dynamic elements will be set to the initial
-values of the parameters that they are assigned to.
-
-``{parameter name}`` specifies the parameter to which the slider or text
-input will be attached to.
-
-[style="{CSS statements}"] specifies valid CSS styling. It will be passed
-directly to the browser without any parsing.
-
-There is a one-to-one relationship between a slider and a parameter.
-I.e. for one parameter you can put only one ```` in the
-```` section. However, you don't have to specify a slider - they
-are optional.
-
-There is a many-to-one relationship between text inputs and a
-parameter. I.e. for one parameter you can put many '' elements in
-the ```` section. However, you don't have to specify a text
-input - they are optional.
-
-You can put only one ```` in the ```` section. It is not
-required.
-
-
-Slider tag
-..........
-
-Slider tag must have ``var`` attribute and optional ``style`` attribute::
-
-
-
-After processing, slider tags will be replaced by jQuery UI sliders with applied
-``style`` attribute.
-
-``var`` attribute must correspond to a parameter. Parameters can be used in any
-of the ``function`` tags in ``functions`` tag. By moving slider, value of
-parameter ``a`` will change, and so result of function, that depends on parameter
-``a``, will also change.
-
-
-Textbox tag
-...........
-
-Texbox tag must have ``var`` attribute and optional ``style`` attribute::
-
-
-
-After processing, textbox tags will be replaced by html text inputs with applied
-``style`` attribute. If you want a readonly text input, then you should use a
-dynamic element instead (see section below "HTML tagsd with ID").
-
-``var`` attribute must correspond to a parameter. Parameters can be used in any
-of the ``function`` tags in ``functions`` tag. By changing the value on the text input,
-value of parameter ``a`` will change, and so result of function, that depends on
-parameter ``a``, will also change.
-
-
-Plot tag
-........
-
-Plot tag may have optional ``style`` attribute::
-
-
-
-After processing plot tags will be replaced by Flot JS plot with applied
-``style`` attribute.
-
-
-HTML tags with ID (dynamic elements)
-....................................
-
-Any HTML tag with ID, e.g. ```` can be used as a
-place where result of function can be inserted. To insert function result to
-an element, element ID must be included in ``function`` tag as ``el_id`` attribute
-and ``output`` value must be ``"element"``::
-
-
- function add(a, b, precision) {
- var x = Math.pow(10, precision || 2);
- return (Math.round(a * x) + Math.round(b * x)) / x;
- }
-
- return add(a, b, 5);
-
-
-
-Configuration tag
------------------
-
-The configuration tag contains parameter settings, graph
-settings, and function definitions which are to be plotted on the
-graph and that use specified parameters.
-
-Configuration tag contains two mandatory tag ``functions`` and ``parameters`` and
-may contain another ``plot`` tag.
-
-
-Parameters tag
-..............
-
-``Parameters`` tag contains ``parameter`` tags. Each ``parameter`` tag must have
-``var``, ``max``, ``min``, ``step`` and ``initial`` attributes::
-
-
-
-
-
-
-``var`` attribute links min, max, step and initial values to parameter name.
-
-``min`` attribute is the minimal value that a parameter can take. Slider and input
-values can not go below it.
-
-``max`` attribute is the maximal value that a parameter can take. Slider and input
-values can not go over it.
-
-``step`` attribute is value of slider step. When a slider increase or decreases
-the specified parameter, it will do so by the amount specified with 'step'
-
-``initial`` attribute is the initial value that the specified parameter should be
-set to. Sliders and inputs will initially show this value.
-
-The parameter's name is specified by the ``var`` property. All occurrences
-of sliders and/or text inputs that specify a ``var`` property, will be
-connected to this parameter - i.e. they will reflect the current
-value of the parameter, and will be updated when the parameter
-changes.
-
-If at lest one of these attributes is not set, then the parameter
-will not be used, slider's and/or text input elements that specify
-this parameter will not be activated, and the specified functions
-which use this parameter will not return a numeric value. This means
-that neglecting to specify at least one of the attributes for some
-parameter will have the result of the whole GST instance not working
-properly.
-
-
-Functions tag
-.............
-
-For the GST to do something, you must defined at least one
-function, which can use any of the specified parameter values. The
-function expects to take the ``x`` value, do some calculations, and
-return the ``y`` value. I.e. this is a 2D plot in Cartesian
-coordinates. This is how the default function is meant to be used for
-the graph.
-
-There are other special cases of functions. They are used mainly for
-outputting to elements, plot labels, or for custom output. Because
-the return a single value, and that value is meant for a single element,
-these function are invoked only with the set of all of the parameters.
-I.e. no ``x`` value is available inside them. They are useful for
-showing the current value of a parameter, showing complex static
-formulas where some parameter's value must change, and other useful
-things.
-
-The different style of function is specified by the ``output`` attribute.
-
-Each function must be defined inside ``function`` tag in ``functions`` tag::
-
-
-
- function add(a, b, precision) {
- var x = Math.pow(10, precision || 2);
- return (Math.round(a * x) + Math.round(b * x)) / x;
- }
-
- return add(a, b, 5);
-
-
-
-The parameter names (along with their values, as provided from text
-inputs and/or sliders), will be available inside all defined
-functions. A defined function body string will be parsed internally
-by the browser's JavaScript engine and converted to a true JS
-function.
-
-The function's parameter list will automatically be created and
-populated, and will include the ``x`` (when ``output`` is not specified or
-is set to ``"graph"``), and all of the specified parameter values (from sliders
-and text inputs). This means that each of the defined functions will have
-access to all of the parameter values. You don't have to use them, but
-they will be there.
-
-Examples::
-
-
- return x;
-
-
-
- return (x + a) * Math.sin(x * b);
-
-
-
- function helperFunc(c1) {
- return c1 * c1 - a;
- }
- return helperFunc(x + 10 * a * b) + Math.sin(a - x);
-
-
-Required parameters::
-
- function body:
-
- A string composing a normal JavaScript function
- except that there is no function declaration
- (along with parameters), and no closing bracket.
-
- So if you normally would have written your
- JavaScript function like this:
-
- function myFunc(x, a, b) {
- return x * a + b;
- }
-
- here you must specify just the function body
- (everything that goes between '{' and '}'). So,
- you would specify the above function like so (the
- bare-bone minimum):
-
- return x * a + b;
-
- VERY IMPORTANT: Because the function will be passed
- to the browser as a single string, depending on implementation
- specifics, the end-of-line characters can be stripped. This
- means that single line JavaScript comments (starting with "//")
- can lead to the effect that everything after the first such comment
- will be treated as a comment. Therefore, it is absolutely
- necessary that such single line comments are not used when
- defining functions for GST. You can safely use the alternative
- multiple line JavaScript comments (such comments start with "/*"
- and end with "*/).
-
- VERY IMPORTANT: If you have a large function body, and decide to
- split it into several lines, than you must wrap it in "CDATA" like
- so:
-
-
-
-
-
-Optional parameters::
-
-
- color: Color name ('red', 'green', etc.) or in the form of
- '#FFFF00'. If not specified, a default color (different
- one for each graphed function) will be given by Flot JS.
- line: A string - 'true' or 'false'. Should the data points be
- connected by a line on the graph? Default is 'true'.
- dot: A string - 'true' or 'false'. Should points be shown for
- each data point on the graph? Default is 'false'.
- bar: A string - 'true' or 'false'. When set to 'true', points
- will be plotted as bars.
- label: A string. If provided, will be shown in the legend, along
- with the color that was used to plot the function.
- output: 'element', 'none', 'plot_label', or 'graph'. If not defined,
- function will be plotted (same as setting 'output' to 'graph').
- If defined, and other than 'graph', function will not be
- plotted, but it's output will be inserted into the element
- with ID specified by 'el_id' attribute.
- el_id: Id of HTML element, defined in '' section. Value of
- function will be inserted as content of this element.
- disable_auto_return: By default, if JavaScript function string is written
- without a "return" statement, the "return" will be
- prepended to it. Set to "true" to disable this
- functionality. This is done so that simple functions
- can be defined in an easy fashion (for example, "a",
- which will be translated into "return a").
- update_on: A string - 'change', or 'slide'. Default (if not set) is
- 'slide'. This defines the event on which a given function is
- called, and its result is inserted into an element. This
- setting is relevant only when "output" is other than "graph".
-
-When specifying ``el_id``, it is essential to set "output" to one of
- element - GST will invoke the function, and the return of it will be
- inserted into a HTML element with id specified by ``el_id``.
- none - GST will simply inoke the function. It is left to the instructor
- who writes the JavaScript function body to update all necesary
- HTML elements inside the function, before it exits. This is done
- so that extra steps can be preformed after an HTML element has
- been updated with a value. Note, that because the return value
- from this function is not actually used, it will be tempting to
- omit the "return" statement. However, in this case, the attribute
- "disable_auto_return" must be set to "true" in order to prevent
- GST from inserting a "return" statement automatically.
- plot_label - GST will process all plot labels (which are strings), and
- will replace the all instances of substrings specified by
- ``el_id`` with the returned value of the function. This is
- necessary if you want a label in the graph to have some changing
- number. Because of the nature of Flot JS, it is impossible to
- achieve the same effect by setting the "output" attribute
- to "element", and including a HTML element in the label.
-
-The above values for "output" will tell GST that the function is meant for an
-HTML element (not for graph), and that it should not get an 'x' parameter (along
-with some value).
-
-
-[Note on MathJax and labels]
-............................
-
-Independently of this module, will render all TeX code
-within the ```` section into nice mathematical formulas. Just
-remember to wrap it in one of::
-
- \( and \) - for inline formulas (formulas surrounded by
- standard text)
- \[ and \] - if you want the formula to be a separate line
-
-It is possible to define a label in standard TeX notation. The JS
-library MathJax will work on these labels also because they are
-inserted on top of the plot as standard HTML (text within a DIV).
-
-If the label is dynamic, i.e. it will contain some text (numeric, or other)
-that has to be updated on a parameter's change, then one can define
-a special function to handle this. The "output" of such a function must be
-set to "none", and the JavaScript code inside this function must update the
-MathJax element by itself. Before exiting, MathJax typeset function should
-be called so that the new text will be re-rendered by MathJax. For example,
-
-
- ...
-
-
- ...
-
-
-
- ...
-
-
-Plot tag
-........
-
-``Plot`` tag inside ``configuration`` tag defines settings for plot output.
-
-Required parameters::
-
- xrange: 2 functions that must return value. Value is constant (3.1415)
- or depend on parameter from parameters section:
-
- return 0;
- return 30;
-
- or
-
- return -a;
- return a;
-
-
- All functions will be calculated over domain between xrange:min
- and xrange:max. Xrange depending on parameter is extremely
- useful when domain(s) of your function(s) depends on parameter
- (like circle, when parameter is radius and you want to allow
- to change it).
-
-Optional parameters::
-
- num_points: Number of data points to generated for the plot. If
- this is not set, the number of points will be
- calculated as width / 5.
-
- bar_width: If functions are present which are to be plotted as bars,
- then this parameter specifies the width of the bars. A
- numeric value for this parameter is expected.
-
- bar_align: If functions are present which are to be plotted as bars,
- then this parameter specifies how to align the bars relative
- to the tick. Available values are "left" and "center".
-
- xticks,
- yticks: 3 floating point numbers separated by commas. This
- specifies how many ticks are created, what number they
- start at, and what number they end at. This is different
- from the 'xrange' setting in that it has nothing to do
- with the data points - it control what area of the
- Cartesian space you will see. The first number is the
- first tick's value, the second number is the step
- between each tick, the third number is the value of the
- last tick. If these configurations are not specified,
- Flot will chose them for you based on the data points
- set that he is currently plotting. Usually, this results
- in a nice graph, however, sometimes you need to fine
- grain the controls. For example, when you want to show
- a fixed area of the Cartesian space, even when the data
- set changes. On it's own, Flot will recalculate the
- ticks, which will result in a different graph each time.
- By specifying the xticks, yticks configurations, only
- the plotted data will change - the axes (ticks) will
- remain as you have defined them.
-
- xticks_names, yticks_names:
- A JSON string which represents a mapping of xticks, yticks
- values to some defined strings. If specified, the graph will
- not have any xticks, yticks except those for which a string
- value has been defined in the JSON string. Note that the
- matching will be string-based and not numeric. I.e. if a tick
- value was "3.70" before, then inside the JSON there should be
- a mapping like {..., "3.70": "Some string", ...}. Example:
-
-
-
-
-
-
-
-
-
- xunits,
- yunits: Units values to be set on axes. Use MathJax. Example:
- \(cm\)
- \(m\)
-
- moving_label:
- A way to specify a label that should be positioned dynamically,
- based on the values of some parameters, or some other factors.
- It is similar to a , but it is only valid for a plot
- because it is drawn relative to the plot coordinate system.
-
- Multiple "moving_label" configurations can be provided, each one
- with a unique text and a unique set of functions that determine
- it's dynamic positioning.
-
- Each "moving_label" can have a "color" attribute (CSS color notation),
- and a "weight" attribute. "weight" can be one of "normal" or "bold",
- and determines the styling of moving label's text.
-
- Each "moving_label" function should return an object with a 'x'
- and 'y properties. Within those functions, all of the parameter
- names along with their value are available.
-
- Example (note that "return" statement is missing; it will be automatically
- inserted by GST):
-
-
-
-
-
There are two kinds of dynamic lables.
- 1) Dynamic changing values in graph legends.
- 2) Dynamic labels, which coordinates depend on parameters
-
a:
-
-
b:
-
-
-
-
-
-
-
-
-
-
- a * x + b
-
- a
-
-
- 030
- 10
- 0, 6, 30
- -9, 1, 9
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/source/gst_example_dynamic_range.xml b/docs/source/gst_example_dynamic_range.xml
deleted file mode 100644
index 0ce4263d62..0000000000
--- a/docs/source/gst_example_dynamic_range.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
Graphic slider tool: Dynamic range and implicit functions.
-
-
You can make x range (not ticks of x axis) of functions to depend on
- parameter value. This can be useful when function domain depends
- on parameter.
-
Also implicit functons like circle can be plotted as 2 separate
- functions of same color.
-
-
-
-
-
-
-
-
-
-
-
- Math.sqrt(a * a - x * x)
- -Math.sqrt(a * a - x * x)
-
-
-
-
- -a
- a
-
- 1000
- -30, 6, 30
- -30, 6, 30
-
-
-
-
diff --git a/docs/source/gst_example_html_element_output.xml b/docs/source/gst_example_html_element_output.xml
deleted file mode 100644
index 340783871a..0000000000
--- a/docs/source/gst_example_html_element_output.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
- A simple equation
- \(
- y_1 = 10 \times b \times \frac{sin(a \times x) \times sin(b \times x)}{cos(b \times x) + 10}
- \)
- can be plotted.
-
-
-
-
-
Currently \(a\) is
-
-
-
-
-
This one
- \(
- y_2 = sin(a \times x)
- \)
- will be overlayed on top.
-
-
-
Currently \(b\) is
-
-
-
-
To change \(a\) use:
-
-
-
-
To change \(b\) use:
-
-
-
-
-
Second input for b:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- return 10.0 * b * Math.sin(a * x) * Math.sin(b * x) / (Math.cos(b * x) + 10);
-
-
-
- Math.sin(a * x);
-
-
- function helperFunc(c1) {
- return c1 * c1 - a;
- }
-
- return helperFunc(x + 10 * a * b) + Math.sin(a - x);
-
- a
-
-
-
-
-
- return 0;
-
- 30
-
-
- 120
-
- 0, 3, 30
- -1.5, 1.5, 13.5
-
- \(cm\)
- \(m\)
-
-
-
-
diff --git a/docs/source/index.rst b/docs/source/index.rst
index d2082ff3a0..eceb5e23e8 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -14,7 +14,6 @@ Contents:
overview.rst
common-lib.rst
djangoapps.rst
- xml_formats.rst
Indices and tables
==================
diff --git a/docs/source/xml_formats.rst b/docs/source/xml_formats.rst
deleted file mode 100644
index 7c92546a5e..0000000000
--- a/docs/source/xml_formats.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-XML formats of Inputtypes and Xmodule
-=====================================
-Contents:
-
-.. toctree::
- :maxdepth: 2
-
- graphical_slider_tool.rst
- drag_and_drop_input.rst
diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py
index 8fb2843656..7d41637c8e 100644
--- a/lms/djangoapps/courseware/features/common.py
+++ b/lms/djangoapps/courseware/features/common.py
@@ -1,11 +1,8 @@
from lettuce import world, step
-from django.core.management import call_command
from nose.tools import assert_equals, assert_in
from lettuce.django import django_url
-from django.conf import settings
from django.contrib.auth.models import User
from student.models import CourseEnrollment
-from terrain.factories import CourseFactory, ItemFactory
from xmodule.modulestore import Location
from xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates
@@ -77,7 +74,8 @@ def should_see_in_the_page(step, text):
@step('I am logged in$')
def i_am_logged_in(step):
world.create_user('robot')
- world.log_in('robot@edx.org', 'test')
+ world.log_in('robot', 'test')
+ world.browser.visit(django_url('/'))
@step('I am not logged in$')
@@ -101,17 +99,17 @@ def create_course(step, course):
# Create the course
# We always use the same org and display name,
# but vary the course identifier (e.g. 600x or 191x)
- course = CourseFactory.create(org=TEST_COURSE_ORG,
- number=course,
- display_name=TEST_COURSE_NAME)
+ course = world.CourseFactory.create(org=TEST_COURSE_ORG,
+ number=course,
+ display_name=TEST_COURSE_NAME)
# Add a section to the course to contain problems
- section = ItemFactory.create(parent_location=course.location,
- display_name=TEST_SECTION_NAME)
+ section = world.ItemFactory.create(parent_location=course.location,
+ display_name=TEST_SECTION_NAME)
- problem_section = ItemFactory.create(parent_location=section.location,
- template='i4x://edx/templates/sequential/Empty',
- display_name=TEST_SECTION_NAME)
+ problem_section = world.ItemFactory.create(parent_location=section.location,
+ template='i4x://edx/templates/sequential/Empty',
+ display_name=TEST_SECTION_NAME)
@step(u'I am registered for the course "([^"]*)"$')
@@ -124,16 +122,17 @@ def i_am_registered_for_the_course(step, course):
u = User.objects.get(username='robot')
# If the user is not already enrolled, enroll the user.
+ # TODO: change to factory
CourseEnrollment.objects.get_or_create(user=u, course_id=course_id(course))
- world.log_in('robot@edx.org', 'test')
+ world.log_in('robot', 'test')
@step(u'The course "([^"]*)" has extra tab "([^"]*)"$')
def add_tab_to_course(step, course, extra_tab_name):
- section_item = ItemFactory.create(parent_location=course_location(course),
- template="i4x://edx/templates/static_tab/Empty",
- display_name=str(extra_tab_name))
+ section_item = world.ItemFactory.create(parent_location=course_location(course),
+ template="i4x://edx/templates/static_tab/Empty",
+ display_name=str(extra_tab_name))
@step(u'I am an edX user$')
@@ -161,7 +160,7 @@ def flush_xmodule_store():
def course_id(course_num):
return "%s/%s/%s" % (TEST_COURSE_ORG, course_num,
- TEST_COURSE_NAME.replace(" ", "_"))
+ TEST_COURSE_NAME.replace(" ", "_"))
def course_location(course_num):
diff --git a/lms/djangoapps/courseware/features/courses.py b/lms/djangoapps/courseware/features/courses.py
index 4fbbfd24f2..c99fb58b85 100644
--- a/lms/djangoapps/courseware/features/courses.py
+++ b/lms/djangoapps/courseware/features/courses.py
@@ -83,13 +83,13 @@ def get_courseware_with_tabs(course_id):
course = get_course_by_id(course_id)
chapters = [chapter for chapter in course.get_children() if not chapter.lms.hide_from_toc]
courseware = [{'chapter_name': c.display_name_with_default,
- 'sections': [{'section_name': s.display_name_with_default,
+ 'sections': [{'section_name': s.display_name_with_default,
'clickable_tab_count': len(s.get_children()) if (type(s) == seq_module.SequenceDescriptor) else 0,
'tabs': [{'children_count': len(t.get_children()) if (type(t) == vertical_module.VerticalDescriptor) else 0,
- 'class': t.__class__.__name__}
- for t in s.get_children()]}
+ 'class': t.__class__.__name__}
+ for t in s.get_children()]}
for s in c.get_children() if not s.lms.hide_from_toc]}
- for c in chapters]
+ for c in chapters]
return courseware
@@ -168,7 +168,6 @@ def process_section(element, num_tabs=0):
assert False, "Class for element not recognized!!"
-
def process_problem(element, problem_id):
'''
Process problem attempts to
diff --git a/lms/djangoapps/courseware/features/high-level-tabs.feature b/lms/djangoapps/courseware/features/high-level-tabs.feature
index 931281a455..473f3f1572 100644
--- a/lms/djangoapps/courseware/features/high-level-tabs.feature
+++ b/lms/djangoapps/courseware/features/high-level-tabs.feature
@@ -6,7 +6,7 @@ Feature: All the high level tabs should work
Scenario: I can navigate to all high -level tabs in a course
Given: I am registered for the course "6.002x"
And The course "6.002x" has extra tab "Custom Tab"
- And I log in
+ And I am logged in
And I click on View Courseware
When I click on the "" tab
Then the page title should contain ""
diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature
index a7fbac49c7..efeb338c45 100644
--- a/lms/djangoapps/courseware/features/problems.feature
+++ b/lms/djangoapps/courseware/features/problems.feature
@@ -1,10 +1,11 @@
-Feature: Answer choice problems
+Feature: Answer problems
As a student in an edX course
In order to test my understanding of the material
- I want to answer choice based problems
+ I want to answer problems
Scenario: I can answer a problem correctly
- Given I am viewing a "" problem
+ Given External graders respond "correct"
+ And I am viewing a "" problem
When I answer a "" problem "correctly"
Then My "" answer is marked "correct"
@@ -17,9 +18,11 @@ Feature: Answer choice problems
| numerical |
| formula |
| script |
+ | code |
Scenario: I can answer a problem incorrectly
- Given I am viewing a "" problem
+ Given External graders respond "incorrect"
+ And I am viewing a "" problem
When I answer a "" problem "incorrectly"
Then My "" answer is marked "incorrect"
@@ -32,6 +35,7 @@ Feature: Answer choice problems
| numerical |
| formula |
| script |
+ | code |
Scenario: I can submit a blank answer
Given I am viewing a "" problem
diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py
index a6575c3d22..6b2239c38b 100644
--- a/lms/djangoapps/courseware/features/problems.py
+++ b/lms/djangoapps/courseware/features/problems.py
@@ -1,14 +1,14 @@
from lettuce import world, step
from lettuce.django import django_url
-from selenium.webdriver.support.ui import Select
import random
import textwrap
+import time
from common import i_am_registered_for_the_course, TEST_SECTION_NAME, section_location
-from terrain.factories import ItemFactory
from capa.tests.response_xml_factory import OptionResponseXMLFactory, \
- ChoiceResponseXMLFactory, MultipleChoiceResponseXMLFactory, \
- StringResponseXMLFactory, NumericalResponseXMLFactory, \
- FormulaResponseXMLFactory, CustomResponseXMLFactory
+ ChoiceResponseXMLFactory, MultipleChoiceResponseXMLFactory, \
+ StringResponseXMLFactory, NumericalResponseXMLFactory, \
+ FormulaResponseXMLFactory, CustomResponseXMLFactory, \
+ CodeResponseXMLFactory
# Factories from capa.tests.response_xml_factory that we will use
# to generate the problem XML, with the keyword args used to configure
@@ -78,6 +78,12 @@ PROBLEM_FACTORY_DICT = {
a2=0
return (a1+a2)==int(expect)
""")}},
+ 'code': {
+ 'factory': CodeResponseXMLFactory(),
+ 'kwargs': {
+ 'question_text': 'Submit code to an external grader',
+ 'initial_display': 'print "Hello world!"',
+ 'grader_payload': '{"grader": "ps1/Spring2013/test_grader.py"}', }},
}
@@ -92,11 +98,11 @@ def add_problem_to_course(course, problem_type):
# Create a problem item using our generated XML
# We set rerandomize=always in the metadata so that the "Reset" button
# will appear.
- problem_item = ItemFactory.create(parent_location=section_location(course),
- template="i4x://edx/templates/problem/Blank_Common_Problem",
- display_name=str(problem_type),
- data=problem_xml,
- metadata={'rerandomize': 'always'})
+ problem_item = world.ItemFactory.create(parent_location=section_location(course),
+ template="i4x://edx/templates/problem/Blank_Common_Problem",
+ display_name=str(problem_type),
+ data=problem_xml,
+ metadata={'rerandomize': 'always'})
@step(u'I am viewing a "([^"]*)" problem')
@@ -116,6 +122,19 @@ def view_problem(step, problem_type):
world.browser.visit(url)
+@step(u'External graders respond "([^"]*)"')
+def set_external_grader_response(step, correctness):
+ assert(correctness in ['correct', 'incorrect'])
+
+ response_dict = {'correct': True if correctness == 'correct' else False,
+ 'score': 1 if correctness == 'correct' else 0,
+ 'msg': 'Your problem was graded %s' % correctness}
+
+ # Set the fake xqueue server to always respond
+ # correct/incorrect when asked to grade a problem
+ world.xqueue_server.set_grade_response(response_dict)
+
+
@step(u'I answer a "([^"]*)" problem "([^"]*)ly"')
def answer_problem(step, problem_type, correctness):
""" Mark a given problem type correct or incorrect, then submit it.
@@ -169,18 +188,66 @@ def answer_problem(step, problem_type, correctness):
inputfield('script', input_num=1).fill(str(first_addend))
inputfield('script', input_num=2).fill(str(second_addend))
+ elif problem_type == 'code':
+ # The fake xqueue server is configured to respond
+ # correct / incorrect no matter what we submit.
+ # Furthermore, since the inline code response uses
+ # JavaScript to make the code display nicely, it's difficult
+ # to programatically input text
+ # (there's not