fix merge conflicts

This commit is contained in:
Your Name
2013-04-22 12:54:33 -04:00
172 changed files with 7233 additions and 5511 deletions

View File

@@ -34,16 +34,23 @@ load-plugins=
# multiple time (only on the command line, not in the configuration file where
# it should appear only once).
disable=
# Never going to use these
# C0301: Line too long
# W0141: Used builtin function 'map'
# W0142: Used * or ** magic
# W0141: Used builtin function 'map'
# Might use these when the code is in better shape
# C0302: Too many lines in module
# R0201: Method could be a function
# R0901: Too many ancestors
# R0902: Too many instance attributes
# R0903: Too few public methods (1/2)
# R0904: Too many public methods
# R0911: Too many return statements
# R0912: Too many branches
# R0913: Too many arguments
C0301,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913
# R0914: Too many local variables
C0301,C0302,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0911,R0912,R0913,R0914
[REPORTS]
@@ -92,7 +99,18 @@ zope=no
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed. Python regular
# expressions are accepted.
generated-members=REQUEST,acl_users,aq_parent,objects,DoesNotExist,can_read,can_write,get_url,size
generated-members=
REQUEST,
acl_users,
aq_parent,
objects,
DoesNotExist,
can_read,
can_write,
get_url,
size,
content,
status_code
[BASIC]

View File

@@ -22,5 +22,4 @@ libreadline6
libreadline6-dev
mongodb
nodejs
npm
coffeescript

View File

@@ -1,6 +1,3 @@
import logging
import sys
from django.contrib.auth.models import User, Group
from django.core.exceptions import PermissionDenied
@@ -131,7 +128,7 @@ def remove_user_from_course_group(caller, user, location, role):
raise PermissionDenied
# see if the user is actually in that role, if not then we don't have to do anything
if is_user_in_course_group_role(user, location, role) == True:
if is_user_in_course_group_role(user, location, role):
groupname = get_course_groupname_for_role(location, role)
group = Group.objects.get(name=groupname)

View File

@@ -97,8 +97,7 @@ def update_course_updates(location, update, passed_id=None):
if (len(new_html_parsed) == 1):
content = new_html_parsed[0].tail
else:
content = "\n".join([html.tostring(ele)
for ele in new_html_parsed[1:]])
content = "\n".join([html.tostring(ele) for ele in new_html_parsed[1:]])
return {"id": passed_id,
"date": update['date'],

View File

@@ -11,6 +11,7 @@ Feature: Advanced (manual) course policy
Given I am on the Advanced Course Settings page in Studio
Then the settings are alphabetized
@skip-phantom
Scenario: Test cancel editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
@@ -19,6 +20,7 @@ Feature: Advanced (manual) course policy
And I reload the page
Then the policy key value is unchanged
@skip-phantom
Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key and save
@@ -26,6 +28,7 @@ Feature: Advanced (manual) course policy
And I reload the page
Then the policy key value is changed
@skip-phantom
Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio
When I create a JSON object as a value
@@ -33,6 +36,7 @@ Feature: Advanced (manual) course policy
And I reload the page
Then it is displayed as formatted
@skip-phantom
Scenario: Test automatic quoting of non-JSON values
Given I am on the Advanced Course Settings page in Studio
When I create a non-JSON value not in quotes

View File

@@ -3,10 +3,7 @@
from lettuce import world, step
from common import *
import time
from terrain.steps import reload_the_page
from nose.tools import assert_true, assert_false, assert_equal
from nose.tools import assert_false, assert_equal
"""
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html
@@ -18,8 +15,8 @@ VALUE_CSS = 'textarea.json'
DISPLAY_NAME_KEY = "display_name"
DISPLAY_NAME_VALUE = '"Robot Super Course"'
############### ACTIONS ####################
############### ACTIONS ####################
@step('I select the Advanced Settings$')
def i_select_advanced_settings(step):
expand_icon_css = 'li.nav-course-settings i.icon-expand'
@@ -38,7 +35,7 @@ def i_am_on_advanced_course_settings(step):
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name):
css = 'a.%s-button' % name.lower()
world.css_click_at(css)
world.css_click(css)
@step(u'I edit the value of a policy key$')
@@ -52,7 +49,7 @@ def edit_the_value_of_a_policy_key(step):
@step(u'I edit the value of a policy key and save$')
def edit_the_value_of_a_policy_key(step):
def edit_the_value_of_a_policy_key_and_save(step):
change_display_name_value(step, '"foo"')
@@ -90,7 +87,7 @@ def it_is_formatted(step):
@step('it is displayed as a string')
def it_is_formatted(step):
def it_is_displayed_as_string(step):
assert_policy_entries([DISPLAY_NAME_KEY], ['"quote me"'])

View File

@@ -10,6 +10,8 @@ Feature: Course checklists
Then I can check and uncheck tasks in a checklist
And They are correctly selected after I reload the page
@skip-phantom
@skip-firefox
Scenario: A task can link to a location within Studio
Given I have opened Checklists
When I select a link to the course outline
@@ -17,8 +19,9 @@ Feature: Course checklists
And I press the browser back button
Then I am brought back to the course outline in the correct state
@skip-phantom
@skip-firefox
Scenario: A task can link to a location outside Studio
Given I have opened Checklists
When I select a link to help page
Then I am brought to the help page in a new window

View File

@@ -6,6 +6,7 @@ from nose.tools import assert_true, assert_equal
from terrain.steps import reload_the_page
from selenium.common.exceptions import StaleElementReferenceException
############### ACTIONS ####################
@step('I select Checklists from the Tools menu$')
def i_select_checklists(step):
@@ -88,8 +89,6 @@ def i_am_brought_to_help_page_in_new_window(step):
assert_equal('http://help.edge.edx.org/', world.browser.url)
############### HELPER METHODS ####################
def verifyChecklist2Status(completed, total, percentage):
def verify_count(driver):
@@ -106,9 +105,11 @@ def verifyChecklist2Status(completed, total, percentage):
def toggleTask(checklist, task):
world.css_click('#course-checklist' + str(checklist) +'-task' + str(task))
world.css_click('#course-checklist' + str(checklist) + '-task' + str(task))
# TODO: figure out a way to do this in phantom and firefox
# For now we will mark the scenerios that use this method as skipped
def clickActionLink(checklist, task, actionText):
# toggle checklist item to make sure that the link button is showing
toggleTask(checklist, task)
@@ -120,4 +121,3 @@ def clickActionLink(checklist, task, actionText):
world.wait_for(verify_action_link_text)
action_link.click()

View File

@@ -1,17 +1,20 @@
Feature: Course Settings
As a course author, I want to be able to configure my course settings.
@skip-phantom
Scenario: User can set course dates
Given I have opened a new course in Studio
When I select Schedule and Details
And I set course dates
Then I see the set dates on refresh
@skip-phantom
Scenario: User can clear previously set course dates (except start date)
Given I have set course dates
And I clear all the dates except start
Then I see cleared dates on refresh
@skip-phantom
Scenario: User cannot clear the course start date
Given I have set course dates
And I clear the course start date

View File

@@ -3,6 +3,7 @@ Feature: Create Section
As a course author
I want to create and edit sections
@skip-phantom
Scenario: Add a new section to a course
Given I have opened a new course in Studio
When I click the New Section link

View File

@@ -1,32 +1,33 @@
Feature: Overview Toggle Section
In order to quickly view the details of a course's section or to scan the inventory of sections
As a course author
I want to toggle the visibility of each section's subsection details in the overview listing
As a course author
I want to toggle the visibility of each section's subsection details in the overview listing
Scenario: The default layout for the overview page is to show sections in expanded view
Given I have a course with multiple sections
When I navigate to the course overview page
Then I see the "Collapse All Sections" link
And all sections are expanded
When I navigate to the course overview page
Then I see the "Collapse All Sections" link
And all sections are expanded
Scenario: Expand /collapse for a course with no sections
Given I have a course with no sections
When I navigate to the course overview page
Then I do not see the "Collapse All Sections" link
When I navigate to the course overview page
Then I do not see the "Collapse All Sections" link
@skip-phantom
Scenario: Collapse link appears after creating first section of a course
Given I have a course with no sections
When I navigate to the course overview page
And I add a section
Then I see the "Collapse All Sections" link
And all sections are expanded
When I navigate to the course overview page
And I add a section
Then I see the "Collapse All Sections" link
And all sections are expanded
@skip-phantom
Scenario: Collapse link is not removed after last section of a course is deleted
Given I have a course with 1 section
And I navigate to the course overview page
When I press the "section" delete icon
And I confirm the alert
And I navigate to the course overview page
When I press the "section" delete icon
And I confirm the alert
Then I see the "Collapse All Sections" link
Scenario: Collapsing all sections when all sections are expanded
@@ -57,4 +58,4 @@ Feature: Overview Toggle Section
When I expand the first section
And I click the "Expand All Sections" link
Then I see the "Collapse All Sections" link
And all sections are expanded
And all sections are expanded

View File

@@ -3,13 +3,15 @@ Feature: Create Subsection
As a course author
I want to create and edit subsections
Scenario: Add a new subsection to a section
@skip-phantom
Scenario: Add a new subsection to a section
Given I have opened a new course section in Studio
When I click the New Subsection link
And I enter the subsection name and click save
Then I see my subsection on the Courseware page
Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216)
@skip-phantom
Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216)
Given I have opened a new course section in Studio
When I click the New Subsection link
And I enter a subsection name with a quote and click save
@@ -17,7 +19,7 @@ Feature: Create Subsection
And I click to edit the subsection name
Then I see the complete subsection name with a quote in the editor
Scenario: Assign grading type to a subsection and verify it is still shown after refresh (bug #258)
Scenario: Assign grading type to a subsection and verify it is still shown after refresh (bug #258)
Given I have opened a new course section in Studio
And I have added a new subsection
And I mark it as Homework
@@ -25,20 +27,19 @@ Feature: Create Subsection
And I reload the page
Then I see it marked as Homework
Scenario: Set a due date in a different year (bug #256)
@skip-phantom
Scenario: Set a due date in a different year (bug #256)
Given I have opened a new subsection in Studio
And I have set a release date and due date in different years
Then I see the correct dates
And I reload the page
Then I see the correct dates
@skip-phantom
Scenario: Delete a subsection
@skip-phantom
Scenario: Delete a subsection
Given I have opened a new course section in Studio
And I have added a new subsection
And I see my subsection on the Courseware page
When I press the "subsection" delete icon
And I confirm the alert
Then the subsection does not exist

View File

@@ -59,7 +59,7 @@ class Command(BaseCommand):
discussion_items = _get_discussion_items(course)
# now query all discussion items via get_items() and compare with the tree-traversal
queried_discussion_items = store.get_items(['i4x', course.location.org, course.location.course,
queried_discussion_items = store.get_items(['i4x', course.location.org, course.location.course,
'discussion', None, None])
for item in queried_discussion_items:

View File

@@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location
from xmodule.course_module import CourseDescriptor
from auth.authz import _copy_course_group
@@ -16,8 +15,7 @@ from auth.authz import _copy_course_group
class Command(BaseCommand):
help = \
'''Clone a MongoDB backed course to another location'''
help = 'Clone a MongoDB backed course to another location'
def handle(self, *args, **options):
if len(args) != 2:

View File

@@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.store_utilities import delete_course
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location
from xmodule.course_module import CourseDescriptor
from .prompt import query_yes_no
@@ -38,7 +37,7 @@ class Command(BaseCommand):
if query_yes_no("Deleting course {0}. Confirm?".format(loc_str), default="no"):
if query_yes_no("Are you sure. This action cannot be undone!", default="no"):
loc = CourseDescriptor.id_to_location(loc_str)
if delete_course(ms, cs, loc, commit) == True:
if delete_course(ms, cs, loc, commit):
print 'removing User permissions from course....'
# in the django layer, we need to remove all the user permissions groups associated with this course
if commit:

View File

@@ -7,7 +7,6 @@ from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location
from xmodule.course_module import CourseDescriptor
@@ -15,8 +14,7 @@ unnamed_modules = 0
class Command(BaseCommand):
help = \
'''Import the specified data directory into the default ModuleStore'''
help = 'Import the specified data directory into the default ModuleStore'
def handle(self, *args, **options):
if len(args) != 2:

View File

@@ -12,8 +12,7 @@ unnamed_modules = 0
class Command(BaseCommand):
help = \
'''Import the specified data directory into the default ModuleStore'''
help = 'Import the specified data directory into the default ModuleStore'
def handle(self, *args, **options):
if len(args) == 0:
@@ -28,4 +27,4 @@ class Command(BaseCommand):
data=data_dir,
courses=course_dirs)
import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False,
static_content_store=contentstore(), verbose=True)
static_content_store=contentstore(), verbose=True)

View File

@@ -11,8 +11,8 @@ def query_yes_no(question, default="yes"):
The "answer" return value is one of "yes" or "no".
"""
valid = {"yes":True, "y":True, "ye":True,
"no":False, "n":False}
valid = {"yes": True, "y": True, "ye": True,
"no": False, "n": False}
if default is None:
prompt = " [y/n] "
elif default == "yes":
@@ -30,5 +30,4 @@ def query_yes_no(question, default="yes"):
elif choice in valid:
return valid[choice]
else:
sys.stdout.write("Please respond with 'yes' or 'no' "\
"(or 'y' or 'n').\n")
sys.stdout.write("Please respond with 'yes' or 'no' (or 'y' or 'n').\n")

View File

@@ -1,9 +1,9 @@
from xmodule.templates import update_templates
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = \
'''Imports and updates the Studio component templates from the code pack and put in the DB'''
help = 'Imports and updates the Studio component templates from the code pack and put in the DB'
def handle(self, *args, **options):
update_templates()
update_templates()

View File

@@ -1,7 +1,5 @@
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.xml_importer import perform_xlint
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
unnamed_modules = 0
@@ -9,10 +7,11 @@ unnamed_modules = 0
class Command(BaseCommand):
help = \
'''
Verify the structure of courseware as to it's suitability for import
To run test: rake cms:xlint DATA_DIR=../data [COURSE_DIR=content-edx-101 (optional parameter)]
'''
'''
Verify the structure of courseware as to it's suitability for import
To run test: rake cms:xlint DATA_DIR=../data [COURSE_DIR=content-edx-101 (optional parameter)]
'''
def handle(self, *args, **options):
if len(args) == 0:
raise CommandError("import requires at least one argument: <data directory> [<course dir>...]")

View File

@@ -1,7 +1,6 @@
from static_replace import replace_static_urls
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location
from django.http import Http404
def get_module_info(store, location, parent_location=None, rewrite_static_links=False):

View File

@@ -6,16 +6,18 @@ from django.conf import settings
from django.core.urlresolvers import reverse
from path import path
from tempdir import mkdtemp_clean
from datetime import timedelta
import json
from fs.osfs import OSFS
import copy
from json import loads
import traceback
from datetime import timedelta
from django.contrib.auth.models import User
from django.dispatch import Signal
from contentstore.utils import get_modulestore
from contentstore.tests.utils import parse_json
from .utils import ModuleStoreTestCase, parse_json
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore import Location
@@ -37,6 +39,7 @@ TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
class MongoCollectionFindWrapper(object):
def __init__(self, original):
self.original = original
@@ -46,6 +49,7 @@ class MongoCollectionFindWrapper(object):
self.counter = self.counter+1
return self.original(query, *args, **kwargs)
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
class ContentStoreToyCourseTest(ModuleStoreTestCase):
"""
@@ -92,6 +96,33 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
return cnt
def test_get_items(self):
'''
This verifies a bug we had where the None setting in get_items() meant 'wildcard'
Unfortunately, None = published for the revision field, so get_items() would return
both draft and non-draft copies.
'''
store = modulestore()
draft_store = modulestore('draft')
import_from_xml(store, 'common/test/data/', ['simple'])
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
draft_store.clone_item(html_module.location, html_module.location)
# now query get_items() to get this location with revision=None, this should just
# return back a single item (not 2)
items = store.get_items(['i4x', 'edX', 'simple', 'html', 'test_html', None])
self.assertEqual(len(items), 1)
self.assertFalse(getattr(items[0], 'is_draft', False))
# now refetch from the draft store. Note that even though we pass
# None in the revision field, the draft store will replace that with 'draft'
items = draft_store.get_items(['i4x', 'edX', 'simple', 'html', 'test_html', None])
self.assertEqual(len(items), 1)
self.assertTrue(getattr(items[0], 'is_draft', False))
def test_draft_metadata(self):
'''
This verifies a bug we had where inherited metadata was getting written to the
@@ -158,32 +189,37 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_get_depth_with_drafts(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
'course', '2012_Fall', None]), depth=None)
course = modulestore('draft').get_item(
Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]),
depth=None
)
# make sure no draft items have been returned
num_drafts = self._get_draft_counts(course)
self.assertEqual(num_drafts, 0)
problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
'problem', 'ps01-simple', None]))
problem = modulestore('draft').get_item(
Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None])
)
# put into draft
modulestore('draft').clone_item(problem.location, problem.location)
# make sure we can query that item and verify that it is a draft
draft_problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
'problem', 'ps01-simple', None]))
self.assertTrue(getattr(draft_problem,'is_draft', False))
draft_problem = modulestore('draft').get_item(
Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None])
)
self.assertTrue(getattr(draft_problem, 'is_draft', False))
#now requery with depth
course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
'course', '2012_Fall', None]), depth=None)
course = modulestore('draft').get_item(
Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]),
depth=None
)
# make sure just one draft item have been returned
num_drafts = self._get_draft_counts(course)
self.assertEqual(num_drafts, 1)
self.assertEqual(num_drafts, 1)
def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -215,13 +251,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
module_store = modulestore('direct')
found = False
item = None
items = module_store.get_items(['i4x', 'edX', 'full', 'poll_question', None, None])
found = len(items) > 0
self.assertTrue(found)
# check that there's actually content in the 'question' field
self.assertGreater(len(items[0].question),0)
self.assertGreater(len(items[0].question), 0)
def test_xlint_fails(self):
err_cnt = perform_xlint('common/test/data', ['full'])
@@ -234,14 +269,16 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
sequential = module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
self.assertTrue(sequential.location.url() in chapter.children)
self.client.post(reverse('delete_item'),
self.client.post(
reverse('delete_item'),
json.dumps({'id': sequential.location.url(), 'delete_children': 'true', 'delete_all_versions': 'true'}),
"application/json")
"application/json"
)
found = False
try:
@@ -252,7 +289,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertFalse(found)
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
self.assertFalse(sequential.location.url() in chapter.children)
@@ -275,7 +312,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
content_store = contentstore()
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
course = module_store.get_item(source_location)
@@ -288,7 +324,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
}
}
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -347,17 +383,48 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_export_course(self):
module_store = modulestore('direct')
draft_store = modulestore('draft')
content_store = contentstore()
import_from_xml(module_store, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
# get a vertical (and components in it) to put into 'draft'
vertical = module_store.get_item(Location(['i4x', 'edX', 'full',
'vertical', 'vertical_66', None]), depth=1)
draft_store.clone_item(vertical.location, vertical.location)
# We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case.
draft_store.clone_item(vertical.location, Location(['i4x', 'edX', 'full',
'vertical', 'no_references', 'draft']))
for child in vertical.get_children():
draft_store.clone_item(child.location, child.location)
root_dir = path(mkdtemp_clean())
# now create a private vertical
private_vertical = draft_store.clone_item(vertical.location,
Location(['i4x', 'edX', 'full', 'vertical', 'a_private_vertical', None]))
# add private to list of children
sequential = module_store.get_item(Location(['i4x', 'edX', 'full',
'sequential', 'Administrivia_and_Circuit_Elements', None]))
private_location_no_draft = private_vertical.location._replace(revision=None)
module_store.update_children(sequential.location, sequential.children +
[private_location_no_draft.url()])
# read back the sequential, to make sure we have a pointer to
sequential = module_store.get_item(Location(['i4x', 'edX', 'full',
'sequential', 'Administrivia_and_Circuit_Elements', None]))
self.assertIn(private_location_no_draft.url(), sequential.children)
print 'Exporting to tempdir = {0}'.format(root_dir)
# export out to a tempdir
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
export_to_xml(module_store, content_store, location, root_dir, 'test_export', draft_modulestore=draft_store)
# check for static tabs
self.verify_content_existence(module_store, root_dir, location, 'tabs', 'static_tab', '.html')
@@ -391,20 +458,36 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
delete_course(module_store, content_store, location)
# reimport
import_from_xml(module_store, root_dir, ['test_export'])
import_from_xml(module_store, root_dir, ['test_export'], draft_store=draft_store)
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertGreater(len(items), 0)
for descriptor in items:
print "Checking {0}....".format(descriptor.location.url())
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
self.assertEqual(resp.status_code, 200)
# don't try to look at private verticals. Right now we're running
# the service in non-draft aware
if getattr(descriptor, 'is_draft', False):
print "Checking {0}....".format(descriptor.location.url())
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
self.assertEqual(resp.status_code, 200)
# verify that we have the content in the draft store as well
vertical = draft_store.get_item(Location(['i4x', 'edX', 'full',
'vertical', 'vertical_66', None]), depth=1)
self.assertTrue(getattr(vertical, 'is_draft', False))
for child in vertical.get_children():
self.assertTrue(getattr(child, 'is_draft', False))
# verify that we have the private vertical
test_private_vertical = draft_store.get_item(Location(['i4x', 'edX', 'full',
'vertical', 'vertical_66', None]))
self.assertTrue(getattr(test_private_vertical, 'is_draft', False))
shutil.rmtree(root_dir)
def test_course_handouts_rewrites(self):
module_store = modulestore('direct')
content_store = contentstore()
# import a test course
import_from_xml(module_store, 'common/test/data/', ['full'])
@@ -437,11 +520,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# make sure we pre-fetched a known sequential which should be at depth=2
self.assertTrue(Location(['i4x', 'edX', 'full', 'sequential',
'Administrivia_and_Circuit_Elements', None]) in course.system.module_data)
'Administrivia_and_Circuit_Elements', None]) in course.system.module_data)
# make sure we don't have a specific vertical which should be at depth=3
self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58',
None]) in course.system.module_data)
self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58', None])
in course.system.module_data)
def test_export_course_with_unknown_metadata(self):
module_store = modulestore('direct')
@@ -463,14 +546,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
print 'Exporting to tempdir = {0}'.format(root_dir)
# export out to a tempdir
exported = False
try:
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
exported = True
except Exception:
pass
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
self.assertTrue(exported)
class ContentStoreTest(ModuleStoreTestCase):
"""
@@ -506,7 +583,7 @@ class ContentStoreTest(ModuleStoreTestCase):
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
}
}
def test_create_course(self):
"""Test new course creation - happy path"""
@@ -533,7 +610,7 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 200)
self.assertEqual(data['ErrMsg'],
'There is already a course defined with the same organization and course number.')
'There is already a course defined with the same organization and course number.')
def test_create_course_with_bad_organization(self):
"""Test new course creation - error path for bad organization name"""
@@ -543,16 +620,18 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 200)
self.assertEqual(data['ErrMsg'],
"Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.")
"Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.")
def test_course_index_view_with_no_courses(self):
"""Test viewing the index page with no courses"""
# Create a course so there is something to view
resp = self.client.get(reverse('index'))
self.assertContains(resp,
self.assertContains(
resp,
'<h1 class="title-1">My Courses</h1>',
status_code=200,
html=True)
html=True
)
def test_course_factory(self):
"""Test that the course factory works correctly."""
@@ -569,26 +648,30 @@ class ContentStoreTest(ModuleStoreTestCase):
"""Test viewing the index page with an existing course"""
CourseFactory.create(display_name='Robot Super Educational Course')
resp = self.client.get(reverse('index'))
self.assertContains(resp,
self.assertContains(
resp,
'<span class="class-name">Robot Super Educational Course</span>',
status_code=200,
html=True)
html=True
)
def test_course_overview_view_with_course(self):
"""Test viewing the course overview page with an existing course"""
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
data = {
'org': 'MITx',
'course': '999',
'name': Location.clean('Robot Super Course'),
}
'org': 'MITx',
'course': '999',
'name': Location.clean('Robot Super Course'),
}
resp = self.client.get(reverse('course_index', kwargs=data))
self.assertContains(resp,
self.assertContains(
resp,
'<article class="courseware-overview" data-course-id="i4x://MITx/999/course/Robot_Super_Course">',
status_code=200,
html=True)
html=True
)
def test_clone_item(self):
"""Test cloning an item. E.g. creating a new section"""
@@ -598,14 +681,16 @@ class ContentStoreTest(ModuleStoreTestCase):
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
'template': 'i4x://edx/templates/chapter/Empty',
'display_name': 'Section One',
}
}
resp = self.client.post(reverse('clone_item'), section_data)
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
self.assertRegexpMatches(data['id'],
'^i4x:\/\/MITx\/999\/chapter\/([0-9]|[a-f]){32}$')
self.assertRegexpMatches(
data['id'],
r"^i4x://MITx/999/chapter/([0-9]|[a-f]){32}$"
)
def test_capa_module(self):
"""Test that a problem treats markdown specially."""
@@ -614,7 +699,7 @@ class ContentStoreTest(ModuleStoreTestCase):
problem_data = {
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
'template': 'i4x://edx/templates/problem/Blank_Common_Problem'
}
}
resp = self.client.post(reverse('clone_item'), problem_data)
@@ -748,6 +833,45 @@ class ContentStoreTest(ModuleStoreTestCase):
# make sure we found the item (e.g. it didn't error while loading)
self.assertTrue(did_load_item)
def test_forum_id_generation(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
new_component_location = Location('i4x', 'edX', 'full', 'discussion', 'new_component')
source_template_location = Location('i4x', 'edx', 'templates', 'discussion', 'Discussion_Tag')
# crate a new module and add it as a child to a vertical
module_store.clone_item(source_template_location, new_component_location)
new_discussion_item = module_store.get_item(new_component_location)
self.assertNotEquals(new_discussion_item.discussion_id, '$$GUID$$')
def test_update_modulestore_signal_did_fire(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
try:
module_store.modulestore_update_signal = Signal(providing_args=['modulestore', 'course_id', 'location'])
self.got_signal = False
def _signal_hander(modulestore=None, course_id=None, location=None, **kwargs):
self.got_signal = True
module_store.modulestore_update_signal.connect(_signal_hander)
new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component')
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
# crate a new module
module_store.clone_item(source_template_location, new_component_location)
finally:
module_store.modulestore_update_signal = None
self.assertTrue(self.got_signal)
def test_metadata_inheritance(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])

View File

@@ -23,14 +23,14 @@ class CachingTestCase(TestCase):
def test_put_and_get(self):
set_cached_content(self.mockAsset)
self.assertEqual(self.mockAsset.content, get_cached_content(self.unicodeLocation).content,
'should be stored in cache with unicodeLocation')
'should be stored in cache with unicodeLocation')
self.assertEqual(self.mockAsset.content, get_cached_content(self.nonUnicodeLocation).content,
'should be stored in cache with nonUnicodeLocation')
'should be stored in cache with nonUnicodeLocation')
def test_delete(self):
set_cached_content(self.mockAsset)
del_cached_content(self.nonUnicodeLocation)
self.assertEqual(None, get_cached_content(self.unicodeLocation),
'should not be stored in cache with unicodeLocation')
'should not be stored in cache with unicodeLocation')
self.assertEqual(None, get_cached_content(self.nonUnicodeLocation),
'should not be stored in cache with nonUnicodeLocation')
'should not be stored in cache with nonUnicodeLocation')

View File

@@ -8,12 +8,11 @@ from django.core.urlresolvers import reverse
from django.utils.timezone import UTC
from xmodule.modulestore import Location
from models.settings.course_details import (CourseDetails,
CourseSettingsEncoder)
from models.settings.course_details import (CourseDetails, CourseSettingsEncoder)
from models.settings.course_grading import CourseGradingModel
from contentstore.utils import get_modulestore
from .utils import ModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from models.settings.course_metadata import CourseMetadata
@@ -21,6 +20,7 @@ from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
from xmodule.fields import Date
class CourseTestCase(ModuleStoreTestCase):
def setUp(self):
"""
@@ -47,12 +47,8 @@ class CourseTestCase(ModuleStoreTestCase):
self.client = Client()
self.client.login(username=uname, password=password)
t = 'i4x://edx/templates/course/Empty'
o = 'MITx'
n = '999'
dn = 'Robot Super Course'
self.course_location = Location('i4x', o, n, 'course', 'Robot_Super_Course')
CourseFactory.create(template=t, org=o, number=n, display_name=dn)
course = CourseFactory.create(template='i4x://edx/templates/course/Empty', org='MITx', number='999', display_name='Robot Super Course')
self.course_location = course.location
class CourseDetailsTestCase(CourseTestCase):
@@ -86,17 +82,25 @@ class CourseDetailsTestCase(CourseTestCase):
jsondetails = CourseDetails.fetch(self.course_location)
jsondetails.syllabus = "<a href='foo'>bar</a>"
# encode - decode to convert date fields and other data which changes form
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).syllabus,
jsondetails.syllabus, "After set syllabus")
self.assertEqual(
CourseDetails.update_from_json(jsondetails.__dict__).syllabus,
jsondetails.syllabus, "After set syllabus"
)
jsondetails.overview = "Overview"
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).overview,
jsondetails.overview, "After set overview")
self.assertEqual(
CourseDetails.update_from_json(jsondetails.__dict__).overview,
jsondetails.overview, "After set overview"
)
jsondetails.intro_video = "intro_video"
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).intro_video,
jsondetails.intro_video, "After set intro_video")
self.assertEqual(
CourseDetails.update_from_json(jsondetails.__dict__).intro_video,
jsondetails.intro_video, "After set intro_video"
)
jsondetails.effort = "effort"
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).effort,
jsondetails.effort, "After set effort")
self.assertEqual(
CourseDetails.update_from_json(jsondetails.__dict__).effort,
jsondetails.effort, "After set effort"
)
class CourseDetailsViewTest(CourseTestCase):
@@ -150,9 +154,7 @@ class CourseDetailsViewTest(CourseTestCase):
@staticmethod
def struct_to_datetime(struct_time):
return datetime.datetime(struct_time.tm_year, struct_time.tm_mon,
struct_time.tm_mday, struct_time.tm_hour,
struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC())
return datetime.datetime(*struct_time[:6], tzinfo=UTC())
def compare_date_fields(self, details, encoded, context, field):
if details[field] is not None:
@@ -249,6 +251,7 @@ class CourseGradingTest(CourseTestCase):
altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2")
class CourseMetadataEditingTest(CourseTestCase):
def setUp(self):
CourseTestCase.setUp(self)
@@ -256,7 +259,6 @@ class CourseMetadataEditingTest(CourseTestCase):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
self.fullcourse_location = Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])
def test_fetch_initial_fields(self):
test_model = CourseMetadata.fetch(self.course_location)
self.assertIn('display_name', test_model, 'Missing editable metadata field')
@@ -271,18 +273,20 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertIn('xqa_key', test_model, 'xqa_key field ')
def test_update_from_json(self):
test_model = CourseMetadata.update_from_json(self.course_location,
{ "advertised_start" : "start A",
"testcenter_info" : { "c" : "test" },
"days_early_for_beta" : 2})
test_model = CourseMetadata.update_from_json(self.course_location, {
"advertised_start": "start A",
"testcenter_info": {"c": "test"},
"days_early_for_beta": 2
})
self.update_check(test_model)
# try fresh fetch to ensure persistence
test_model = CourseMetadata.fetch(self.course_location)
self.update_check(test_model)
# now change some of the existing metadata
test_model = CourseMetadata.update_from_json(self.course_location,
{ "advertised_start" : "start B",
"display_name" : "jolly roger"})
test_model = CourseMetadata.update_from_json(self.course_location, {
"advertised_start": "start B",
"display_name": "jolly roger"}
)
self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value")
self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
@@ -294,13 +298,12 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field')
self.assertEqual(test_model['advertised_start'], 'start A', "advertised_start not expected value")
self.assertIn('testcenter_info', test_model, 'Missing testcenter_info metadata field')
self.assertDictEqual(test_model['testcenter_info'], { "c" : "test" }, "testcenter_info not expected value")
self.assertDictEqual(test_model['testcenter_info'], {"c": "test"}, "testcenter_info not expected value")
self.assertIn('days_early_for_beta', test_model, 'Missing days_early_for_beta metadata field')
self.assertEqual(test_model['days_early_for_beta'], 2, "days_early_for_beta not expected value")
def test_delete_key(self):
test_model = CourseMetadata.delete_key(self.fullcourse_location, { 'deleteKeys' : ['doesnt_exist', 'showanswer', 'xqa_key']})
test_model = CourseMetadata.delete_key(self.fullcourse_location, {'deleteKeys': ['doesnt_exist', 'showanswer', 'xqa_key']})
# ensure no harm
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
self.assertIn('display_name', test_model, 'full missing editable metadata field')

View File

@@ -3,7 +3,7 @@ from contentstore import utils
import mock
from django.test import TestCase
from xmodule.modulestore.tests.factories import CourseFactory
from .utils import ModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
class LMSLinksTestCase(TestCase):
@@ -30,7 +30,7 @@ class LMSLinksTestCase(TestCase):
class UrlReverseTestCase(ModuleStoreTestCase):
""" Tests for get_url_reverse """
def test_CoursePageNames(self):
def test_course_page_names(self):
""" Test the defined course pages. """
course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course')
@@ -69,4 +69,4 @@ class UrlReverseTestCase(ModuleStoreTestCase):
self.assertEquals(
'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about',
utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course)
)
)

View File

@@ -1,30 +1,8 @@
import json
import shutil
from django.test.client import Client
from django.conf import settings
from django.core.urlresolvers import reverse
from path import path
import json
from fs.osfs import OSFS
import copy
from contentstore.utils import get_modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.store_utilities import delete_course
from xmodule.modulestore.django import modulestore, _MODULESTORES
from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor
from xmodule.seq_module import SequenceDescriptor
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from .utils import ModuleStoreTestCase, parse_json, user, registration
from .utils import parse_json, user, registration
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
class ContentStoreTestCase(ModuleStoreTestCase):
@@ -84,6 +62,7 @@ class ContentStoreTestCase(ModuleStoreTestCase):
# Now make sure that the user is now actually activated
self.assertTrue(user(email).is_active)
class AuthTestCase(ContentStoreTestCase):
"""Check that various permissions-related things work"""
@@ -101,9 +80,9 @@ class AuthTestCase(ContentStoreTestCase):
def test_public_pages_load(self):
"""Make sure pages that don't require login load without error."""
pages = (
reverse('login'),
reverse('signup'),
)
reverse('login'),
reverse('signup'),
)
for page in pages:
print "Checking '{0}'".format(page)
self.check_page_get(page, 200)
@@ -136,13 +115,13 @@ class AuthTestCase(ContentStoreTestCase):
"""Make sure pages that do require login work."""
auth_pages = (
reverse('index'),
)
)
# These are pages that should just load when the user is logged in
# (no data needed)
simple_auth_pages = (
reverse('index'),
)
)
# need an activated user
self.test_create_account()

View File

@@ -2,112 +2,11 @@
Utilities for contentstore tests
'''
#pylint: disable=W0603
import json
import copy
from uuid import uuid4
from django.test import TestCase
from django.conf import settings
from student.models import Registration
from django.contrib.auth.models import User
import xmodule.modulestore.django
from xmodule.templates import update_templates
class ModuleStoreTestCase(TestCase):
""" Subclass for any test case that uses the mongodb
module store. This populates a uniquely named modulestore
collection with templates before running the TestCase
and drops it they are finished. """
@staticmethod
def flush_mongo_except_templates():
'''
Delete everything in the module store except templates
'''
modulestore = xmodule.modulestore.django.modulestore()
# This query means: every item in the collection
# that is not a template
query = {"_id.course": {"$ne": "templates"}}
# Remove everything except templates
modulestore.collection.remove(query)
@staticmethod
def load_templates_if_necessary():
'''
Load templates into the modulestore only if they do not already exist.
We need the templates, because they are copied to create
XModules such as sections and problems
'''
modulestore = xmodule.modulestore.django.modulestore()
# Count the number of templates
query = {"_id.course": "templates"}
num_templates = modulestore.collection.find(query).count()
if num_templates < 1:
update_templates()
@classmethod
def setUpClass(cls):
'''
Flush the mongo store and set up templates
'''
# Use a uuid to differentiate
# the mongo collections on jenkins.
cls.orig_modulestore = copy.deepcopy(settings.MODULESTORE)
test_modulestore = cls.orig_modulestore
test_modulestore['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
test_modulestore['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
xmodule.modulestore.django._MODULESTORES = {}
settings.MODULESTORE = test_modulestore
TestCase.setUpClass()
@classmethod
def tearDownClass(cls):
'''
Revert to the old modulestore settings
'''
# Clean up by dropping the collection
modulestore = xmodule.modulestore.django.modulestore()
modulestore.collection.drop()
# Restore the original modulestore settings
settings.MODULESTORE = cls.orig_modulestore
def _pre_setup(self):
'''
Remove everything but the templates before each test
'''
# Flush anything that is not a template
ModuleStoreTestCase.flush_mongo_except_templates()
# Check that we have templates loaded; if not, load them
ModuleStoreTestCase.load_templates_if_necessary()
# Call superclass implementation
super(ModuleStoreTestCase, self)._pre_setup()
def _post_teardown(self):
'''
Flush everything we created except the templates
'''
# Flush anything that is not a template
ModuleStoreTestCase.flush_mongo_except_templates()
# Call superclass implementation
super(ModuleStoreTestCase, self)._post_teardown()
def parse_json(response):
"""Parse response, which is assumed to be json"""

View File

@@ -1,4 +1,3 @@
import logging
from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
@@ -9,7 +8,8 @@ import copy
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
#In order to instantiate an open ended tab automatically, need to have this data
OPEN_ENDED_PANEL = {"name" : "Open Ended Panel", "type" : "open_ended"}
OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"}
def get_modulestore(location):
"""
@@ -86,11 +86,10 @@ def get_lms_link_for_item(location, preview=False, course_id=None):
if settings.LMS_BASE is not None:
if preview:
lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
'preview.' + settings.LMS_BASE)
lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE', 'preview.' + settings.LMS_BASE)
else:
lms_base = settings.LMS_BASE
lms_link = "//{lms_base}/courses/{course_id}/jump_to/{location}".format(
lms_base=lms_base,
course_id=course_id,
@@ -192,6 +191,7 @@ class CoursePageNames:
CourseOutline = "course_index"
Checklists = "checklists"
def add_open_ended_panel_tab(course):
"""
Used to add the open ended panel tab to a course if it does not exist.
@@ -208,6 +208,7 @@ def add_open_ended_panel_tab(course):
changed = True
return changed, course_tabs
def remove_open_ended_panel_tab(course):
"""
Used to remove the open ended panel tab from a course if it exists.
@@ -220,6 +221,6 @@ def remove_open_ended_panel_tab(course):
#Check to see if open ended panel is defined in the course
if OPEN_ENDED_PANEL in course_tabs:
#Add panel to the tabs if it is not defined
course_tabs = [ct for ct in course_tabs if ct!=OPEN_ENDED_PANEL]
course_tabs = [ct for ct in course_tabs if ct != OPEN_ENDED_PANEL]
changed = True
return changed, course_tabs

View File

@@ -14,9 +14,6 @@ from tempfile import mkdtemp
from django.core.servers.basehttp import FileWrapper
from django.core.files.temp import NamedTemporaryFile
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
from PIL import Image
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError
from django.http import HttpResponseNotFound
from django.contrib.auth.decorators import login_required
@@ -244,8 +241,7 @@ def edit_subsection(request, location):
(field.name, field.read_from(item))
for field
in item.fields
if field.name not in ['display_name', 'start', 'due', 'format'] and
field.scope == Scope.settings
if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings
)
can_view_live = False
@@ -257,18 +253,18 @@ def edit_subsection(request, location):
break
return render_to_response('edit_subsection.html',
{'subsection': item,
'context_course': course,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'lms_link': lms_link,
'preview_link': preview_link,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
'parent_location': course.location,
'parent_item': parent,
'policy_metadata': policy_metadata,
'subsection_units': subsection_units,
'can_view_live': can_view_live
})
{'subsection': item,
'context_course': course,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'lms_link': lms_link,
'preview_link': preview_link,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
'parent_location': course.location,
'parent_item': parent,
'policy_metadata': policy_metadata,
'subsection_units': subsection_units,
'can_view_live': can_view_live
})
@login_required
@@ -347,17 +343,17 @@ def edit_unit(request, location):
index = index + 1
preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
'preview.' + settings.LMS_BASE)
'preview.' + settings.LMS_BASE)
preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
preview_lms_base=preview_lms_base,
lms_base=settings.LMS_BASE,
org=course.location.org,
course=course.location.course,
course_name=course.location.name,
section=containing_section.location.name,
subsection=containing_subsection.location.name,
index=index)
preview_lms_base=preview_lms_base,
lms_base=settings.LMS_BASE,
org=course.location.org,
course=course.location.course,
course_name=course.location.name,
section=containing_section.location.name,
subsection=containing_subsection.location.name,
index=index)
unit_state = compute_unit_state(item)
@@ -623,7 +619,6 @@ def delete_item(request):
store = get_modulestore(item_loc)
# @TODO: this probably leaves draft items dangling. My preferance would be for the semantic to be
# if item.location.revision=None, then delete both draft and published version
# if caller wants to only delete the draft than the caller should put item.location.revision='draft'
@@ -665,7 +660,7 @@ def save_item(request):
if not has_access(request.user, item_location):
raise PermissionDenied()
store = get_modulestore(Location(item_location));
store = get_modulestore(Location(item_location))
if request.POST.get('data') is not None:
data = request.POST['data']
@@ -800,7 +795,7 @@ def upload_asset(request, org, course, coursename):
# Does the course actually exist?!? Get anything from it to prove its existance
try:
item = modulestore().get_item(location)
modulestore().get_item(location)
except:
# no return it as a Bad Request response
logging.error('Could not find course' + location)
@@ -834,24 +829,23 @@ def upload_asset(request, org, course, coursename):
readback = contentstore().find(content.location)
response_payload = {'displayname': content.name,
'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
'url': StaticContent.get_url_path_from_location(content.location),
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
'msg': 'Upload completed'
}
'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
'url': StaticContent.get_url_path_from_location(content.location),
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
'msg': 'Upload completed'
}
response = HttpResponse(json.dumps(response_payload))
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
return response
'''
This view will return all CMS users who are editors for the specified course
'''
@login_required
@ensure_csrf_cookie
def manage_users(request, location):
'''
This view will return all CMS users who are editors for the specified course
'''
# check that logged in user has permissions to this item
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
raise PermissionDenied()
@@ -878,14 +872,14 @@ def create_json_response(errmsg=None):
return resp
'''
This POST-back view will add a user - specified by email - to the list of editors for
the specified course
'''
@expect_json
@login_required
@ensure_csrf_cookie
def add_user(request, location):
'''
This POST-back view will add a user - specified by email - to the list of editors for
the specified course
'''
email = request.POST["email"]
if email == '':
@@ -911,14 +905,15 @@ def add_user(request, location):
return create_json_response()
'''
This POST-back view will remove a user - specified by email - from the list of editors for
the specified course
'''
@expect_json
@login_required
@ensure_csrf_cookie
def remove_user(request, location):
'''
This POST-back view will remove a user - specified by email - from the list of editors for
the specified course
'''
email = request.POST["email"]
# check that logged in user has admin permissions on this course
@@ -993,13 +988,12 @@ def reorder_static_tabs(request):
for tab in course.tabs:
if tab['type'] == 'static_tab':
reordered_tabs.append({'type': 'static_tab',
'name': tab_items[static_tab_idx].display_name,
'url_slug': tab_items[static_tab_idx].location.name})
'name': tab_items[static_tab_idx].display_name,
'url_slug': tab_items[static_tab_idx].location.name})
static_tab_idx += 1
else:
reordered_tabs.append(tab)
# OK, re-assemble the static tabs in the new order
course.tabs = reordered_tabs
modulestore('direct').update_metadata(course.location, own_metadata(course))
@@ -1011,7 +1005,6 @@ def reorder_static_tabs(request):
def edit_tabs(request, org, course, coursename):
location = ['i4x', org, course, 'course', coursename]
course_item = modulestore().get_item(location)
static_tabs_loc = Location('i4x', org, course, 'static_tab', None)
# check that logged in user has permissions to this item
if not has_access(request.user, location):
@@ -1040,7 +1033,7 @@ def edit_tabs(request, org, course, coursename):
'active_tab': 'pages',
'context_course': course_item,
'components': components
})
})
def not_found(request):
@@ -1102,21 +1095,21 @@ def course_info_updates(request, org, course, provided_id=None):
if request.method == 'GET':
return HttpResponse(json.dumps(get_course_updates(location)),
mimetype="application/json")
mimetype="application/json")
elif real_method == 'DELETE':
try:
return HttpResponse(json.dumps(delete_course_update(location,
request.POST, provided_id)), mimetype="application/json")
request.POST, provided_id)), mimetype="application/json")
except:
return HttpResponseBadRequest("Failed to delete",
content_type="text/plain")
content_type="text/plain")
elif request.method == 'POST':
try:
return HttpResponse(json.dumps(update_course_updates(location,
request.POST, provided_id)), mimetype="application/json")
request.POST, provided_id)), mimetype="application/json")
except:
return HttpResponseBadRequest("Failed to save",
content_type="text/plain")
content_type="text/plain")
@expect_json
@@ -1184,7 +1177,7 @@ def course_config_graders_page(request, org, course, name):
return render_to_response('settings_graders.html', {
'context_course': course_module,
'course_location' : location,
'course_location': location,
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
})
@@ -1203,8 +1196,8 @@ def course_config_advanced_page(request, org, course, name):
return render_to_response('settings_advanced.html', {
'context_course': course_module,
'course_location' : location,
'advanced_dict' : json.dumps(CourseMetadata.fetch(location)),
'course_location': location,
'advanced_dict': json.dumps(CourseMetadata.fetch(location)),
})
@@ -1225,7 +1218,8 @@ def course_settings_updates(request, org, course, name, section):
manager = CourseDetails
elif section == 'grading':
manager = CourseGradingModel
else: return
else:
return
if request.method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
@@ -1320,6 +1314,7 @@ def course_advanced_updates(request, org, course, name):
response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs))
return HttpResponse(response_json, mimetype="application/json")
@ensure_csrf_cookie
@login_required
def get_checklists(request, org, course, name):
@@ -1345,10 +1340,10 @@ def get_checklists(request, org, course, name):
if copied or modified:
modulestore.update_metadata(location, own_metadata(course_module))
return render_to_response('checklists.html',
{
'context_course': course_module,
'checklists': checklists
})
{
'context_course': course_module,
'checklists': checklists
})
@ensure_csrf_cookie
@@ -1433,7 +1428,6 @@ def asset_index(request, org, course, name):
# sort in reverse upload date order
assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
thumbnails = contentstore().get_all_content_thumbnails_for_course(course_reference)
asset_display = []
for asset in assets:
id = asset['_id']
@@ -1527,10 +1521,10 @@ def initialize_course_tabs(course):
# This logic is repeated in xmodule/modulestore/tests/factories.py
# so if you change anything here, you need to also change it there.
course.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
@@ -1586,7 +1580,10 @@ def import_course(request, org, course, name):
shutil.move(r / fname, course_dir)
module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
[course_subdir], load_error_modules=False, static_content_store=contentstore(), target_location_namespace=Location(location))
[course_subdir], load_error_modules=False,
static_content_store=contentstore(),
target_location_namespace=Location(location),
draft_store=modulestore())
# we can blow this away when we're done importing.
shutil.rmtree(course_dir)
@@ -1620,8 +1617,8 @@ def generate_export_course(request, org, course, name):
logging.debug('root = {0}'.format(root_dir))
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name)
# filename = root_dir / name + '.tar.gz'
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
#filename = root_dir / name + '.tar.gz'
logging.debug('tar file being generated at {0}'.format(export_file.name))
tf = tarfile.open(name=export_file.name, mode='w:gz')

View File

@@ -174,7 +174,6 @@ class CourseDetails(object):
return result
# TODO move to a more general util? Is there a better way to do the isinstance model check?
class CourseSettingsEncoder(json.JSONEncoder):
def default(self, obj):

View File

@@ -45,14 +45,13 @@ class CourseGradingModel(object):
# return empty model
else:
return {
"id": index,
return {"id": index,
"type": "",
"min_count": 0,
"drop_count": 0,
"short_label": None,
"weight": 0
}
}
@staticmethod
def fetch_cutoffs(course_location):
@@ -95,7 +94,6 @@ class CourseGradingModel(object):
return CourseGradingModel.fetch(course_location)
@staticmethod
def update_grader_from_json(course_location, grader):
"""
@@ -137,7 +135,6 @@ class CourseGradingModel(object):
return cutoffs
@staticmethod
def update_grace_period_from_json(course_location, graceperiodjson):
"""
@@ -210,8 +207,7 @@ class CourseGradingModel(object):
location = Location(location)
descriptor = get_modulestore(location).get_item(location)
return {
"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
return {"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
"location": location,
"id": 99 # just an arbitrary value to
}
@@ -231,7 +227,6 @@ class CourseGradingModel(object):
get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata)
@staticmethod
def convert_set_grace_period(descriptor):
# 5 hours 59 minutes 59 seconds => converted to iso format
@@ -262,13 +257,12 @@ class CourseGradingModel(object):
@staticmethod
def parse_grader(json_grader):
# manual to clear out kruft
result = {
"type": json_grader["type"],
"min_count": int(json_grader.get('min_count', 0)),
"drop_count": int(json_grader.get('drop_count', 0)),
"short_label": json_grader.get('short_label', None),
"weight": float(json_grader.get('weight', 0)) / 100.0
}
result = {"type": json_grader["type"],
"min_count": int(json_grader.get('min_count', 0)),
"drop_count": int(json_grader.get('drop_count', 0)),
"short_label": json_grader.get('short_label', None),
"weight": float(json_grader.get('weight', 0)) / 100.0
}
return result

View File

@@ -6,6 +6,7 @@ from xblock.core import Scope
from xmodule.course_module import CourseDescriptor
import copy
class CourseMetadata(object):
'''
For CRUD operations on metadata fields which do not have specific editors
@@ -13,8 +14,13 @@ class CourseMetadata(object):
The objects have no predefined attrs but instead are obj encodings of the
editable metadata.
'''
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end',
'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', 'checklists']
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start',
'end',
'enrollment_start',
'enrollment_end',
'tabs',
'graceperiod',
'checklists']
@classmethod
def fetch(cls, course_location):
@@ -48,7 +54,7 @@ class CourseMetadata(object):
descriptor = get_modulestore(course_location).get_item(course_location)
dirty = False
#Copy the filtered list to avoid permanently changing the class attribute
filtered_list = copy.copy(cls.FILTERED_LIST)
#Don't filter on the tab attribute if filter_tabs is False
@@ -71,7 +77,7 @@ class CourseMetadata(object):
if dirty:
get_modulestore(course_location).update_metadata(course_location,
own_metadata(descriptor))
own_metadata(descriptor))
# Could just generate and return a course obj w/o doing any db reads,
# but I put the reads in as a means to confirm it persisted correctly
@@ -92,6 +98,6 @@ class CourseMetadata(object):
delattr(descriptor.lms, key)
get_modulestore(course_location).update_metadata(course_location,
own_metadata(descriptor))
own_metadata(descriptor))
return cls.fetch(course_location)

View File

@@ -36,3 +36,4 @@ DATABASES = {
INSTALLED_APPS += ('lettuce.django',)
LETTUCE_APPS = ('contentstore',)
LETTUCE_SERVER_PORT = 8001
LETTUCE_BROWSER = 'chrome'

View File

@@ -67,4 +67,4 @@ MODULESTORE = AUTH_TOKENS['MODULESTORE']
CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE']
# Datadog for events!
DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
DATADOG_API = AUTH_TOKENS.get("DATADOG_API")

View File

@@ -167,7 +167,7 @@ STATICFILES_DIRS = [
PROJECT_ROOT / "static",
# This is how you would use the textbook images locally
# ("book", ENV_ROOT / "book_images")
# ("book", ENV_ROOT / "book_images")
]
# Locale/Internationalization

View File

@@ -13,7 +13,6 @@ from path import path
# Nose Test Runner
INSTALLED_APPS += ('django_nose',)
NOSE_ARGS = ['--with-xunit']
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
TEST_ROOT = path('test_root')
@@ -28,7 +27,7 @@ GITHUB_REPO_ROOT = TEST_ROOT / "data"
COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data"
# Makes the tests run much faster...
SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
# TODO (cpennington): We need to figure out how envs/test.py can inject things into common.py so that we don't have to repeat this sort of thing
STATICFILES_DIRS = [
@@ -41,7 +40,7 @@ STATICFILES_DIRS += [
if os.path.isdir(COMMON_TEST_DATA_ROOT / course_dir)
]
modulestore_options = {
MODULESTORE_OPTIONS = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
@@ -53,15 +52,15 @@ modulestore_options = {
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options
'OPTIONS': MODULESTORE_OPTIONS
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options
'OPTIONS': MODULESTORE_OPTIONS
},
'draft': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': modulestore_options
'OPTIONS': MODULESTORE_OPTIONS
}
}
@@ -76,7 +75,7 @@ CONTENTSTORE = {
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "cms.db",
'NAME': TEST_ROOT / "db" / "cms.db",
},
}
@@ -121,3 +120,7 @@ PASSWORD_HASHERS = (
# dummy segment-io key
SEGMENT_IO_KEY = '***REMOVED***'
# disable NPS survey in test mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False

View File

@@ -1,9 +1,10 @@
from dogapi import dog_http_api, dog_stats_api
from django.conf import settings
from xmodule.modulestore.django import modulestore
from django.dispatch import Signal
from request_cache.middleware import RequestCache
from django.core.cache import get_cache, InvalidCacheBackendError
from django.core.cache import get_cache
cache = get_cache('mongo_metadata_inheritance')
for store_name in settings.MODULESTORE:
@@ -11,6 +12,8 @@ for store_name in settings.MODULESTORE:
store.metadata_inheritance_cache_subsystem = cache
store.request_cache = RequestCache.get_request_cache()
modulestore_update_signal = Signal(providing_args=['modulestore', 'course_id', 'location'])
store.modulestore_update_signal = modulestore_update_signal
if hasattr(settings, 'DATADOG_API'):
dog_http_api.api_key = settings.DATADOG_API
dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True)

View File

@@ -1,3 +1,5 @@
if (!window.CmsUtils) window.CmsUtils = {};
var $body;
var $modal;
var $modalCover;
@@ -91,7 +93,7 @@ $(document).ready(function () {
$('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink);
// tender feedback window scrolling
$('a.show-tender').bind('click', smoothScrollTop);
$('a.show-tender').bind('click', window.CmsUtils.smoothScrollTop);
// toggling footer additional support
$('.cta-show-sock').bind('click', toggleSock);
@@ -172,7 +174,10 @@ function smoothScrollLink(e) {
});
}
function smoothScrollTop(e) {
// On AWS instances, this base.js gets wrapped in a separate scope as part of Django static
// pipelining (note, this doesn't happen on local runtimes). So if we set it on window,
// when we can access it from other scopes (namely Course Advanced Settings).
window.CmsUtils.smoothScrollTop = function (e) {
(e).preventDefault();
$.smoothScroll({
@@ -220,7 +225,6 @@ function toggleSections(e) {
function editSectionPublishDate(e) {
e.preventDefault();
$modal = $('.edit-subsection-publish-settings').show();
$modal = $('.edit-subsection-publish-settings').show();
$modal.attr('data-id', $(this).attr('data-id'));
$modal.find('.start-date').val($(this).attr('data-date'));
$modal.find('.start-time').val($(this).attr('data-time'));

View File

@@ -130,7 +130,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
}
},
saveView : function(event) {
smoothScrollTop(event);
window.CmsUtils.smoothScrollTop(event);
// TODO one last verification scan:
// call validateKey on each to ensure proper format
// check for dupes

View File

@@ -97,7 +97,7 @@
color: $blue;
&:hover, &:active {
background: $blue-l3;
background: $blue-l4;
color: $blue-s2;
}

View File

@@ -8,11 +8,11 @@ input[type="password"],
textarea.text {
padding: 6px 8px 8px;
@include box-sizing(border-box);
border: 1px solid $mediumGrey;
border: 1px solid $gray-l2;
border-radius: 2px;
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
background-color: $lightGrey;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
@include linear-gradient($gray-l5, $white);
background-color: $gray-l5;
@include box-shadow(inset 0 1px 2px $shadow-l1);
font-family: 'Open Sans', sans-serif;
font-size: 11px;
color: $baseFontColor;
@@ -21,7 +21,7 @@ textarea.text {
&::-webkit-input-placeholder,
&:-moz-placeholder,
&:-ms-input-placeholder {
color: #979faf;
color: $gray-l2;
}
&:focus {
@@ -30,7 +30,72 @@ textarea.text {
}
}
// forms - specific
// ====================
// forms - fields - not editable
.field.is-not-editable {
& label.is-focused {
color: $gray-d2;
}
label, input, textarea {
pointer-events: none;
}
}
// ====================
// field with error
.field.error {
input, textarea {
border-color: $red;
}
}
// ====================
// forms - additional UI
form {
.note {
@include box-sizing(border-box);
.title {
}
.copy {
}
// note with actions
&.has-actions {
@include clearfix();
.title {
}
.copy {
}
.list-actions {
}
}
}
.note-promotion {
}
}
// ====================
// forms - grandfathered
input.search {
padding: 6px 15px 8px 30px;
@include box-sizing(border-box);
@@ -73,4 +138,4 @@ code {
background-color: #edf1f5;
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset);
font-family: Monaco, monospace;
}
}

View File

@@ -4,7 +4,7 @@
body.signup, body.signin {
.wrapper-content {
margin: 0;
margin: ($baseline*1.5) 0 0 0;
padding: 0 $baseline;
position: relative;
width: 100%;
@@ -18,7 +18,7 @@ body.signup, body.signin {
width: flex-grid(12);
margin: 0 auto;
color: $gray-d2;
header {
position: relative;
margin-bottom: $baseline;
@@ -121,7 +121,7 @@ body.signup, body.signin {
@include font-size(16);
height: 100%;
width: 100%;
padding: ($baseline/2);
padding: ($baseline/2);
&.long {
width: 100%;
@@ -136,15 +136,15 @@ body.signup, body.signin {
}
:-moz-placeholder {
color: $gray-l3;
color: $gray-l3;
}
::-moz-placeholder {
color: $gray-l3;
color: $gray-l3;
}
:-ms-input-placeholder {
color: $gray-l3;
:-ms-input-placeholder {
color: $gray-l3;
}
&:focus {

View File

@@ -147,7 +147,7 @@ body.course.settings {
}
label {
@include font-size(14);
@extend .t-copy-sub1;
@include transition(color, 0.15s, ease-in-out);
margin: 0 0 ($baseline/4) 0;
font-weight: 400;
@@ -161,7 +161,7 @@ body.course.settings {
@include placeholder($gray-l4);
@include font-size(16);
@include size(100%,100%);
padding: ($baseline/2);
padding: ($baseline/2);
&.long {
}
@@ -212,7 +212,7 @@ body.course.settings {
padding: $baseline;
&:last-child {
padding-bottom: $baseline;
padding-bottom: $baseline;
}
.actions {
@@ -238,33 +238,36 @@ body.course.settings {
}
}
// not editable fields
.field.is-not-editable {
& label.is-focused {
color: $gray-d2;
}
}
// field with error
.field.error {
input, textarea {
border-color: $red;
}
}
// specific fields - basic
&.basic {
.list-input {
@include clearfix();
padding: 0 ($baseline/2);
.field {
margin-bottom: 0;
}
}
// course details that should appear more like content than elements to change
.field.is-not-editable {
label {
}
input, textarea {
@extend .t-copy-lead1;
@include box-shadow(none);
border: none;
background: none;
padding: 0;
margin: 0;
font-weight: 600;
}
}
#field-course-organization {
float: left;
width: flex-grid(2, 9);
@@ -281,6 +284,58 @@ body.course.settings {
float: left;
width: flex-grid(5, 9);
}
// course link note
.note-promotion-courseURL {
@include box-shadow(0 2px 1px $shadow-l1);
@include border-radius(($baseline/5));
margin-top: ($baseline*1.5);
border: 1px solid $gray-l2;
padding: ($baseline/2) 0 0 0;
.title {
@extend .t-copy-sub1;
margin: 0 0 ($baseline/10) 0;
padding: 0 ($baseline/2);
.tip {
display: inline;
margin-left: ($baseline/4);
}
}
.copy {
padding: 0 ($baseline/2) ($baseline/2) ($baseline/2);
.link-courseURL {
@extend .t-copy-lead1;
&:hover {
}
}
}
.list-actions {
@include box-shadow(inset 0 1px 1px $shadow-l1);
border-top: 1px solid $gray-l2;
padding: ($baseline/2);
background: $gray-l5;
.action-primary {
@include blue-button();
@include font-size(13);
font-weight: 600;
.icon {
@extend .t-icon;
@include font-size(16);
display: inline-block;
vertical-align: middle;
}
}
}
}
}
// specific fields - schedule
@@ -322,7 +377,7 @@ body.course.settings {
}
}
}
// specific fields - overview
#field-course-overview {
@@ -468,7 +523,7 @@ body.course.settings {
}
}
}
.grade-specific-bar {
height: 50px !important;
}
@@ -479,7 +534,7 @@ body.course.settings {
li {
position: absolute;
top: 0;
height: 50px;
height: 50px;
text-align: right;
@include border-radius(2px);
@@ -600,8 +655,8 @@ body.course.settings {
}
#field-course-grading-assignment-shortname,
#field-course-grading-assignment-totalassignments,
#field-course-grading-assignment-gradeweight,
#field-course-grading-assignment-totalassignments,
#field-course-grading-assignment-gradeweight,
#field-course-grading-assignment-droppable {
width: flex-grid(2, 6);
}
@@ -734,4 +789,4 @@ body.course.settings {
.content-supplementary {
width: flex-grid(3, 12);
}
}
}

View File

@@ -4,7 +4,7 @@
<%namespace name='static' file='static_content.html'/>
<%!
from contentstore import utils
from contentstore import utils
%>
@@ -13,17 +13,17 @@ from contentstore import utils
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
<script src="${static.url('js/vendor/date.js')}"></script>
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/settings/main_settings_view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/course_details.js')}"></script>
<script type="text/javascript">
$(document).ready(function(){
// hilighting labels when fields are focused in
$("form :input").focus(function() {
$("label[for='" + this.id + "']").addClass("is-focused");
@@ -32,18 +32,18 @@ from contentstore import utils
});
var model = new CMS.Models.Settings.CourseDetails();
model.urlRoot = '${details_url}';
model.fetch({success :
model.fetch({success :
function(model) {
var editor = new CMS.Views.Settings.Details({
el: $('.settings-details'),
model: model
});
editor.render();
}
});
});
</script>
</%block>
@@ -62,10 +62,10 @@ from contentstore import utils
<article class="content-primary" role="main">
<form id="settings_details" class="settings-details" method="post" action="">
<section class="group-settings basic">
<header>
<header>
<h2 class="title-2">Basic Information</h2>
<span class="tip">The nuts and bolts of your course</span>
</header>
</header>
<ol class="list-input">
<li class="field text is-not-editable" id="field-course-organization">
@@ -83,45 +83,57 @@ from contentstore import utils
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-name" value="[Course Name]" readonly />
</li>
</ol>
<span class="tip tip-stacked">These are used in <a rel="external" href="${utils.get_lms_link_for_about_page(course_location)}" />your course URL</a>, and cannot be changed</span>
<div class="note note-promotion note-promotion-courseURL has-actions">
<h3 class="title">Course Summary Page <span class="tip">(for student enrollment and access)</span></h3>
<div class="copy">
<p><a class="link-courseURL" rel="external" href="https:${utils.get_lms_link_for_about_page(course_location)}" />https:${utils.get_lms_link_for_about_page(course_location)}</a></p>
</div>
<ul class="list-actions">
<li class="action-item">
<a title="Send a note to students via email" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20&quot;${context_course.display_name_with_default}&quot;,%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="ss-icon icon ss-symbolicons-standard icon icon-inline icon-announcement">&#x2709;</i> Invite your students</a>
</li>
</ul>
</div>
</section>
<hr class="divide" />
<hr class="divide" />
<section class="group-settings schedule">
<header>
<header>
<h2 class="title-2">Course Schedule</h2>
<span class="tip">Important steps and segments of your course</span>
</header>
</header>
<ol class="list-input">
<li class="field-group field-group-course-start" id="course-start">
<div class="field date" id="field-course-start-date">
<label for="course-start-date">Course Start Date</label>
<input type="text" class="start-date date start datepicker" id="course-start-date" placeholder="MM/DD/YYYY" autocomplete="off" />
<span class="tip tip-stacked">First day the course begins</span>
</div>
<span class="tip tip-stacked">First day the course begins</span>
</div>
<div class="field time" id="field-course-start-time">
<label for="course-start-time">Course Start Time</label>
<input type="text" class="time start timepicker" id="course-start-time" value="" placeholder="HH:MM" autocomplete="off" />
<span class="tip tip-stacked" id="timezone"></span>
</div>
<span class="tip tip-stacked" id="timezone"></span>
</div>
</li>
<li class="field-group field-group-course-end" id="course-end">
<div class="field date" id="field-course-end-date">
<label for="course-end-date">Course End Date</label>
<input type="text" class="end-date date end" id="course-end-date" placeholder="MM/DD/YYYY" autocomplete="off" />
<span class="tip tip-stacked">Last day your course is active</span>
</div>
<span class="tip tip-stacked">Last day your course is active</span>
</div>
<div class="field time" id="field-course-end-time">
<label for="course-end-time">Course End Time</label>
<input type="text" class="time end" id="course-end-time" value="" placeholder="HH:MM" autocomplete="off" />
<span class="tip tip-stacked" id="timezone"></span>
</div>
</li>
<span class="tip tip-stacked" id="timezone"></span>
</div>
</li>
</ol>
<ol class="list-input">
@@ -129,33 +141,33 @@ from contentstore import utils
<div class="field date" id="field-enrollment-start-date">
<label for="course-enrollment-start-date">Enrollment Start Date</label>
<input type="text" class="start-date date start" id="course-enrollment-start-date" placeholder="MM/DD/YYYY" autocomplete="off" />
<span class="tip tip-stacked">First day students can enroll</span>
</div>
<span class="tip tip-stacked">First day students can enroll</span>
</div>
<div class="field time" id="field-enrollment-start-time">
<label for="course-enrollment-start-time">Enrollment Start Time</label>
<input type="text" class="time start" id="course-enrollment-start-time" value="" placeholder="HH:MM" autocomplete="off" />
<span class="tip tip-stacked" id="timezone"></span>
</div>
<span class="tip tip-stacked" id="timezone"></span>
</div>
</li>
<li class="field-group field-group-enrollment-end" id="enrollment-end">
<div class="field date" id="field-enrollment-end-date">
<label for="course-enrollment-end-date">Enrollment End Date</label>
<input type="text" class="end-date date end" id="course-enrollment-end-date" placeholder="MM/DD/YYYY" autocomplete="off" />
<span class="tip tip-stacked">Last day students can enroll</span>
</div>
<span class="tip tip-stacked">Last day students can enroll</span>
</div>
<div class="field time" id="field-enrollment-end-time">
<label for="course-enrollment-end-time">Enrollment End Time</label>
<input type="text" class="time end" id="course-enrollment-end-time" value="" placeholder="HH:MM" autocomplete="off" />
<span class="tip tip-stacked" id="timezone"></span>
</div>
</li>
<span class="tip tip-stacked" id="timezone"></span>
</div>
</li>
</ol>
</section>
<hr class="divide" />
<hr class="divide" />
<section class="group-settings marketing">
<header>
@@ -167,45 +179,44 @@ from contentstore import utils
<li class="field text" id="field-course-overview">
<label for="course-overview">Course Overview</label>
<textarea class="tinymce text-editor" id="course-overview"></textarea>
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a href="${utils.get_lms_link_for_about_page(course_location)}">your course summary page</a></span>
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a class="link-courseURL" rel="external" href="${utils.get_lms_link_for_about_page(course_location)}">your course summary page</a></span>
</li>
<li class="field video" id="field-course-introduction-video">
<label for="course-overview">Course Introduction Video</label>
<div class="input input-existing">
<div class="current current-course-introduction-video">
<div class="input input-existing">
<div class="current current-course-introduction-video">
<iframe width="618" height="350" src="" frameborder="0" allowfullscreen></iframe>
</div>
</div>
<div class="actions">
<a href="#" class="remove-item remove-course-introduction-video remove-video-data"><span class="delete-icon"></span> Delete Current Video</a>
</div>
</div>
</div>
<div class="input">
<div class="input">
<input type="text" class="long new-course-introduction-video add-video-data" id="course-introduction-video" value="" placeholder="your YouTube video's ID" autocomplete="off" />
<span class="tip tip-stacked">Enter your YouTube video's ID (along with any restriction parameters)</span>
</div>
</div>
</li>
</ol>
</section>
<hr class="divide" />
<hr class="divide" />
<section class="group-settings requirements">
<header>
<header>
<h2 class="title-2">Requirements</h2>
<span class="tip">Expectations of the students taking this course</span>
</header>
</header>
<ol class="list-input">
<li class="field text" id="field-course-effort">
<label for="course-effort">Hours of Effort per Week</label>
<input type="text" class="short time" id="course-effort" placeholder="HH:MM" />
<span class="tip tip-inline">Time spent on all course work</span>
</li>
</ol>
</section>
<span class="tip tip-inline">Time spent on all course work</span>
</li>
</ol>
</section>
</form>
</article>
@@ -215,7 +226,7 @@ from contentstore import utils
<p>Your course's schedule settings determine when students can enroll in and begin a course as well as when the course.</p>
<p>Additionally, details provided on this page are also used in edX's catalog of courses, which new and returning students use to choose new courses to study.</p>
</div>
</div>
<div class="bit">
% if context_course:
@@ -234,4 +245,4 @@ from contentstore import utils
</aside>
</section>
</div>
</%block>
</%block>

View File

@@ -0,0 +1 @@
<%include file="metadata-edit.html" />

View File

@@ -12,7 +12,7 @@
<form id="hls-form" enctype="multipart/form-data">
<section class="source-edit">
<textarea name="" data-metadata-name="source_code" class="source-edit-box hls-data" rows="8" cols="40">${metadata['source_code']|h}</textarea>
<textarea name="" data-metadata-name="source_code" class="source-edit-box hls-data" rows="8" cols="40">${editable_metadata_fields['source_code']|h}</textarea>
</section>
<div class="submit">
<button type="reset" class="hls-compile">Save &amp; Compile to edX XML</button>

View File

@@ -2,17 +2,17 @@ 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 factory import DjangoModelFactory, Factory, SubFactory, PostGenerationMethodCall
from uuid import uuid4
class GroupFactory(Factory):
class GroupFactory(DjangoModelFactory):
FACTORY_FOR = Group
name = 'staff_MITx/999/Robot_Super_Course'
class UserProfileFactory(Factory):
class UserProfileFactory(DjangoModelFactory):
FACTORY_FOR = UserProfile
user = None
@@ -23,19 +23,20 @@ class UserProfileFactory(Factory):
goals = 'World domination'
class RegistrationFactory(Factory):
class RegistrationFactory(DjangoModelFactory):
FACTORY_FOR = Registration
user = None
activation_key = uuid4().hex
class UserFactory(Factory):
class UserFactory(DjangoModelFactory):
FACTORY_FOR = User
username = 'robot'
email = 'robot+test@edx.org'
password = 'test'
password = PostGenerationMethodCall('set_password',
'test')
first_name = 'Robot'
last_name = 'Test'
is_staff = False
@@ -45,14 +46,18 @@ class UserFactory(Factory):
date_joined = datetime(2011, 1, 1)
class CourseEnrollmentFactory(Factory):
class AdminFactory(UserFactory):
is_staff = True
class CourseEnrollmentFactory(DjangoModelFactory):
FACTORY_FOR = CourseEnrollment
user = SubFactory(UserFactory)
course_id = 'edX/toy/2012_Fall'
class CourseEnrollmentAllowedFactory(Factory):
class CourseEnrollmentAllowedFactory(DjangoModelFactory):
FACTORY_FOR = CourseEnrollmentAllowed
email = 'test@edx.org'

View File

@@ -1079,7 +1079,7 @@ def test_center_login(request):
# which contains the error code describing the exceptional condition.
def makeErrorURL(error_url, error_code):
log.error("generating error URL with error code {}".format(error_code))
return "{}?code={}".format(error_url, error_code);
return "{}?code={}".format(error_url, error_code)
# get provided error URL, which will be used as a known prefix for returning error messages to the
# Pearson shell.
@@ -1088,7 +1088,7 @@ def test_center_login(request):
# TODO: check that the parameters have not been tampered with, by comparing the code provided by Pearson
# with the code we calculate for the same parameters.
if 'code' not in request.POST:
return HttpResponseRedirect(makeErrorURL(error_url, "missingSecurityCode"));
return HttpResponseRedirect(makeErrorURL(error_url, "missingSecurityCode"))
code = request.POST.get("code")
# calculate SHA for query string
@@ -1096,7 +1096,7 @@ def test_center_login(request):
if 'clientCandidateID' not in request.POST:
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientCandidateID"));
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientCandidateID"))
client_candidate_id = request.POST.get("clientCandidateID")
# TODO: check remaining parameters, and maybe at least log if they're not matching
@@ -1109,7 +1109,7 @@ def test_center_login(request):
testcenteruser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id)
except TestCenterUser.DoesNotExist:
log.error("not able to find demographics for cand ID {}".format(client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID"));
return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID"))
# find testcenter_registration that matches the provided exam code:
# Note that we could rely in future on either the registrationId or the exam code,
@@ -1120,7 +1120,7 @@ def test_center_login(request):
# so instead of "missingExamSeriesCode", we use a valid one that is
# inaccurate but at least distinct. (Sigh.)
log.error("missing exam series code for cand ID {}".format(client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "missingPartnerID"));
return HttpResponseRedirect(makeErrorURL(error_url, "missingPartnerID"))
exam_series_code = request.POST.get('vueExamSeriesCode')
# special case for supporting test user:
if client_candidate_id == "edX003671291147" and exam_series_code != '6002x001':
@@ -1130,7 +1130,7 @@ def test_center_login(request):
registrations = TestCenterRegistration.objects.filter(testcenter_user=testcenteruser, exam_series_code=exam_series_code)
if not registrations:
log.error("not able to find exam registration for exam {} and cand ID {}".format(exam_series_code, client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned"));
return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned"))
# TODO: figure out what to do if there are more than one registrations....
# for now, just take the first...
@@ -1140,11 +1140,11 @@ def test_center_login(request):
course = course_from_id(course_id) # assume it will be found....
if not course:
log.error("not able to find course from ID {} for cand ID {}".format(course_id, client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"));
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"))
exam = course.get_test_center_exam(exam_series_code)
if not exam:
log.error("not able to find exam {} for course ID {} and cand ID {}".format(exam_series_code, course_id, client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"));
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"))
location = exam.exam_url
log.info("proceeding with test of cand {} on exam {} for course {}: URL = {}".format(client_candidate_id, exam_series_code, course_id, location))
@@ -1152,7 +1152,7 @@ def test_center_login(request):
timelimit_descriptor = modulestore().get_instance(course_id, Location(location))
if not timelimit_descriptor:
log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format(client_candidate_id, exam_series_code, course_id, location))
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"));
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"))
timelimit_module_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user,
timelimit_descriptor, depth=None)
@@ -1160,11 +1160,11 @@ def test_center_login(request):
timelimit_module_cache, course_id, position=None)
if not timelimit_module.category == 'timelimit':
log.error("cand {} on exam {} for course {}: non-timelimit module at location {}".format(client_candidate_id, exam_series_code, course_id, location))
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"));
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"))
if timelimit_module and timelimit_module.has_ended:
log.warning("cand {} on exam {} for course {}: test already over at {}".format(client_candidate_id, exam_series_code, course_id, timelimit_module.ending_at))
return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken"));
return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken"))
# check if we need to provide an accommodation:
time_accommodation_mapping = {'ET12ET' : 'ADDHALFTIME',

View File

@@ -1,6 +1,8 @@
from lettuce import before, after, world
from splinter.browser import Browser
from logging import getLogger
from django.core.management import call_command
from django.conf import settings
# Let the LMS and CMS do their one-time setup
# For example, setting up mongo caches
@@ -10,18 +12,14 @@ from cms import one_time_startup
logger = getLogger(__name__)
logger.info("Loading the lettuce acceptance testing terrain file...")
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')
# world.browser = Browser('firefox')
browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome')
world.browser = Browser(browser_driver)
@before.each_scenario
@@ -34,6 +32,15 @@ def reset_data(scenario):
call_command('flush', interactive=False)
@after.each_scenario
def screenshot_on_error(scenario):
'''
Save a screenshot to help with debugging
'''
if scenario.failed:
world.browser.driver.save_screenshot('/tmp/last_failed_scenario.png')
@after.all
def teardown_browser(total):
'''

View File

@@ -132,6 +132,8 @@ def i_am_logged_in(step):
world.create_user('robot')
world.log_in('robot', 'test')
world.browser.visit(django_url('/'))
# You should not see the login link
assert_equals(world.browser.find_by_css('a#login'), [])
@step(u'I am an edX user$')

View File

@@ -150,8 +150,8 @@ class InputTypeBase(object):
## we can swap this around in the future if there's a more logical
## order.
self.id = state.get('id', xml.get('id'))
if self.id is None:
self.input_id = state.get('id', xml.get('id'))
if self.input_id is None:
raise ValueError("input id state is None. xml is {0}".format(
etree.tostring(xml)))
@@ -249,7 +249,7 @@ class InputTypeBase(object):
and don't need to override this method.
"""
context = {
'id': self.id,
'id': self.input_id,
'value': self.value,
'status': self.status,
'msg': self.msg,
@@ -457,8 +457,21 @@ class TextLine(InputTypeBase):
"""
A text line input. Can do math preview if "math"="1" is specified.
If the hidden attribute is specified, the textline is hidden and the input id is stored in a div with name equal
to the value of the hidden attribute. This is used e.g. for embedding simulations turned into questions.
If "trailing_text" is set to a value, then the textline will be shown with
the value after the text input, and before the checkmark or any input-specific
feedback. HTML will not work, but properly escaped HTML characters will. This
feature is useful if you would like to specify a specific type of units for the
text input.
If the hidden attribute is specified, the textline is hidden and the input id
is stored in a div with name equal to the value of the hidden attribute. This
is used e.g. for embedding simulations turned into questions.
Example:
<texline math="1" trailing_text="m/s" />
This example will render out a text line with a math preview and the text 'm/s'
after the end of the text line.
"""
template = "textline.html"
@@ -483,6 +496,7 @@ class TextLine(InputTypeBase):
Attribute('dojs', None, render=False),
Attribute('preprocessorClassName', None, render=False),
Attribute('preprocessorSrc', None, render=False),
Attribute('trailing_text', ''),
]
def setup(self):
@@ -609,7 +623,6 @@ class CodeInput(InputTypeBase):
self.queue_len = self.msg
self.msg = self.submitted_msg
def setup(self):
''' setup this input type '''
self.setup_code_response_rendering()
@@ -641,7 +654,7 @@ class MatlabInput(CodeInput):
tags = ['matlabinput']
plot_submitted_msg = ("Submitted. As soon as a response is returned, "
"this message will be replaced by that feedback.")
"this message will be replaced by that feedback.")
def setup(self):
'''
@@ -662,16 +675,16 @@ class MatlabInput(CodeInput):
self.queue_len = 1
self.msg = self.plot_submitted_msg
def handle_ajax(self, dispatch, get):
'''
'''
Handle AJAX calls directed to this input
Args:
- dispatch (str) - indicates how we want this ajax call to be handled
- get (dict) - dictionary of key-value pairs that contain useful data
Returns:
dict - 'success' - whether or not we successfully queued this submission
- 'message' - message to be rendered in case of error
'''
if dispatch == 'plot':
@@ -679,7 +692,7 @@ class MatlabInput(CodeInput):
return {}
def ungraded_response(self, queue_msg, queuekey):
'''
'''
Handle the response from the XQueue
Stores the response in the input_state so it can be rendered later
@@ -691,7 +704,7 @@ class MatlabInput(CodeInput):
nothing
'''
# check the queuekey against the saved queuekey
if('queuestate' in self.input_state and self.input_state['queuestate'] == 'queued'
if('queuestate' in self.input_state and self.input_state['queuestate'] == 'queued'
and self.input_state['queuekey'] == queuekey):
msg = self._parse_data(queue_msg)
# save the queue message so that it can be rendered later
@@ -702,9 +715,9 @@ class MatlabInput(CodeInput):
def _extra_context(self):
''' Set up additional context variables'''
extra_context = {
'queue_len': str(self.queue_len),
'queue_msg': self.queue_msg
}
'queue_len': str(self.queue_len),
'queue_msg': self.queue_msg
}
return extra_context
def _parse_data(self, queue_msg):
@@ -719,20 +732,19 @@ class MatlabInput(CodeInput):
result = json.loads(queue_msg)
except (TypeError, ValueError):
log.error("External message should be a JSON serialized dict."
" Received queue_msg = %s" % queue_msg)
" Received queue_msg = %s" % queue_msg)
raise
msg = result['msg']
return msg
def _plot_data(self, get):
'''
'''
AJAX handler for the plot button
Args:
get (dict) - should have key 'submission' which contains the student submission
Returns:
dict - 'success' - whether or not we successfully queued this submission
- 'message' - message to be rendered in case of error
- 'message' - message to be rendered in case of error
'''
# only send data if xqueue exists
if self.system.xqueue is None:
@@ -748,26 +760,25 @@ class MatlabInput(CodeInput):
anonymous_student_id = self.system.anonymous_student_id
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
anonymous_student_id +
self.id)
self.input_id)
xheader = xqueue_interface.make_xheader(
lms_callback_url = callback_url,
lms_key = queuekey,
queue_name = self.queuename)
lms_callback_url=callback_url,
lms_key=queuekey,
queue_name=self.queuename)
# save the input state
self.input_state['queuekey'] = queuekey
self.input_state['queuestate'] = 'queued'
# construct xqueue body
student_info = {'anonymous_student_id': anonymous_student_id,
'submission_time': qtime}
'submission_time': qtime}
contents = {'grader_payload': self.plot_payload,
'student_info': json.dumps(student_info),
'student_response': response}
(error, msg) = qinterface.send_to_queue(header=xheader,
body = json.dumps(contents))
body=json.dumps(contents))
return {'success': error == 0, 'message': msg}
@@ -1026,7 +1037,7 @@ class DragAndDropInput(InputTypeBase):
if tag_type == 'draggable':
dic['target_fields'] = [parse(target, 'target') for target in
tag.iterchildren('target')]
tag.iterchildren('target')]
return dic

View File

@@ -31,6 +31,7 @@
style="display:none;"
% endif
/>
${trailing_text | h}
<p class="status">
% if status == 'unsubmitted':

View File

@@ -156,6 +156,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
'hidden': False,
'do_math': False,
'id': '1_2_1',
'trailing_text': '',
'size': None}
expected_solution_context = {'id': '1_solution_1'}

View File

@@ -0,0 +1,407 @@
"""Tests for the logic in input type mako templates."""
import unittest
import capa
import os.path
from lxml import etree
from mako.template import Template as MakoTemplate
from mako import exceptions
class TemplateError(Exception):
"""Error occurred while rendering a Mako template"""
pass
class TemplateTestCase(unittest.TestCase):
"""Utilitites for testing templates"""
# Subclasses override this to specify the file name of the template
# to be loaded from capa/templates.
# The template name should include the .html extension:
# for example: choicegroup.html
TEMPLATE_NAME = None
def setUp(self):
"""Load the template"""
capa_path = capa.__path__[0]
self.template_path = os.path.join(capa_path,
'templates',
self.TEMPLATE_NAME)
template_file = open(self.template_path)
self.template = MakoTemplate(template_file.read())
template_file.close()
def render_to_xml(self, context_dict):
"""Render the template using the `context_dict` dict.
Returns an `etree` XML element."""
try:
xml_str = self.template.render_unicode(**context_dict)
except:
raise TemplateError(exceptions.text_error_template().render())
return etree.fromstring(xml_str)
def assert_has_xpath(self, xml_root, xpath, context_dict, exact_num=1):
"""Asserts that the xml tree has an element satisfying `xpath`.
`xml_root` is an etree XML element
`xpath` is an XPath string, such as `'/foo/bar'`
`context` is used to print a debugging message
`exact_num` is the exact number of matches to expect.
"""
message = ("XML does not have %d match(es) for xpath '%s'\nXML: %s\nContext: %s"
% (exact_num, str(xpath), etree.tostring(xml_root), str(context_dict)))
self.assertEqual(len(xml_root.xpath(xpath)), exact_num, msg=message)
def assert_no_xpath(self, xml_root, xpath, context_dict):
"""Asserts that the xml tree does NOT have an element
satisfying `xpath`.
`xml_root` is an etree XML element
`xpath` is an XPath string, such as `'/foo/bar'`
`context` is used to print a debugging message
"""
self.assert_has_xpath(xml_root, xpath, context_dict, exact_num=0)
def assert_has_text(self, xml_root, xpath, text, exact=True):
"""Find the element at `xpath` in `xml_root` and assert
that its text is `text`.
`xml_root` is an etree XML element
`xpath` is an XPath string, such as `'/foo/bar'`
`text` is the expected text that the element should contain
If multiple elements are found, checks the first one.
If no elements are found, the assertion fails.
"""
element_list = xml_root.xpath(xpath)
self.assertTrue(len(element_list) > 0,
"Could not find element at '%s'" % str(xpath))
if exact:
self.assertEqual(text, element_list[0].text)
else:
self.assertIn(text, element_list[0].text)
class ChoiceGroupTemplateTest(TemplateTestCase):
"""Test mako template for `<choicegroup>` input"""
TEMPLATE_NAME = 'choicegroup.html'
def setUp(self):
choices = [('1', 'choice 1'), ('2', 'choice 2'), ('3', 'choice 3')]
self.context = {'id': '1',
'choices': choices,
'status': 'correct',
'input_type': 'checkbox',
'name_array_suffix': '1',
'value': '3'}
super(ChoiceGroupTemplateTest, self).setUp()
def test_problem_marked_correct(self):
"""Test conditions under which the entire problem
(not a particular option) is marked correct"""
self.context['status'] = 'correct'
self.context['input_type'] = 'checkbox'
self.context['value'] = ['1', '2']
# Should mark the entire problem correct
xml = self.render_to_xml(self.context)
xpath = "//div[@class='indicator_container']/span[@class='correct']"
self.assert_has_xpath(xml, xpath, self.context)
# Should NOT mark individual options
self.assert_no_xpath(xml, "//label[@class='choicegroup_incorrect']",
self.context)
self.assert_no_xpath(xml, "//label[@class='choicegroup_correct']",
self.context)
def test_problem_marked_incorrect(self):
"""Test all conditions under which the entire problem
(not a particular option) is marked incorrect"""
conditions = [
{'status': 'incorrect', 'input_type': 'radio', 'value': ''},
{'status': 'incorrect', 'input_type': 'checkbox', 'value': []},
{'status': 'incorrect', 'input_type': 'checkbox', 'value': ['2']},
{'status': 'incorrect', 'input_type': 'checkbox', 'value': ['2', '3']},
{'status': 'incomplete', 'input_type': 'radio', 'value': ''},
{'status': 'incomplete', 'input_type': 'checkbox', 'value': []},
{'status': 'incomplete', 'input_type': 'checkbox', 'value': ['2']},
{'status': 'incomplete', 'input_type': 'checkbox', 'value': ['2', '3']}]
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
xpath = "//div[@class='indicator_container']/span[@class='incorrect']"
self.assert_has_xpath(xml, xpath, self.context)
# Should NOT mark individual options
self.assert_no_xpath(xml,
"//label[@class='choicegroup_incorrect']",
self.context)
self.assert_no_xpath(xml,
"//label[@class='choicegroup_correct']",
self.context)
def test_problem_marked_unsubmitted(self):
"""Test all conditions under which the entire problem
(not a particular option) is marked unanswered"""
conditions = [
{'status': 'unsubmitted', 'input_type': 'radio', 'value': ''},
{'status': 'unsubmitted', 'input_type': 'radio', 'value': []},
{'status': 'unsubmitted', 'input_type': 'checkbox', 'value': []},
{'input_type': 'radio', 'value': ''},
{'input_type': 'radio', 'value': []},
{'input_type': 'checkbox', 'value': []},
{'input_type': 'checkbox', 'value': ['1']},
{'input_type': 'checkbox', 'value': ['1', '2']}]
self.context['status'] = 'unanswered'
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
xpath = "//div[@class='indicator_container']/span[@class='unanswered']"
self.assert_has_xpath(xml, xpath, self.context)
# Should NOT mark individual options
self.assert_no_xpath(xml,
"//label[@class='choicegroup_incorrect']",
self.context)
self.assert_no_xpath(xml,
"//label[@class='choicegroup_correct']",
self.context)
def test_option_marked_correct(self):
"""Test conditions under which a particular option
(not the entire problem) is marked correct."""
conditions = [
{'input_type': 'radio', 'value': '2'},
{'input_type': 'radio', 'value': ['2']}]
self.context['status'] = 'correct'
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
xpath = "//label[@class='choicegroup_correct']"
self.assert_has_xpath(xml, xpath, self.context)
# Should NOT mark the whole problem
xpath = "//div[@class='indicator_container']/span"
self.assert_no_xpath(xml, xpath, self.context)
def test_option_marked_incorrect(self):
"""Test conditions under which a particular option
(not the entire problem) is marked incorrect."""
conditions = [
{'input_type': 'radio', 'value': '2'},
{'input_type': 'radio', 'value': ['2']}]
self.context['status'] = 'incorrect'
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
xpath = "//label[@class='choicegroup_incorrect']"
self.assert_has_xpath(xml, xpath, self.context)
# Should NOT mark the whole problem
xpath = "//div[@class='indicator_container']/span"
self.assert_no_xpath(xml, xpath, self.context)
def test_never_show_correctness(self):
"""Test conditions under which we tell the template to
NOT show correct/incorrect, but instead show a message.
This is used, for example, by the Justice course to ask
questions without specifying a correct answer. When
the student responds, the problem displays "Thank you
for your response"
"""
conditions = [
{'input_type': 'radio', 'status': 'correct', 'value': ''},
{'input_type': 'radio', 'status': 'correct', 'value': '2'},
{'input_type': 'radio', 'status': 'correct', 'value': ['2']},
{'input_type': 'radio', 'status': 'incorrect', 'value': '2'},
{'input_type': 'radio', 'status': 'incorrect', 'value': []},
{'input_type': 'radio', 'status': 'incorrect', 'value': ['2']},
{'input_type': 'checkbox', 'status': 'correct', 'value': []},
{'input_type': 'checkbox', 'status': 'correct', 'value': ['2']},
{'input_type': 'checkbox', 'status': 'incorrect', 'value': []},
{'input_type': 'checkbox', 'status': 'incorrect', 'value': ['2']}]
self.context['show_correctness'] = 'never'
self.context['submitted_message'] = 'Test message'
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
# Should NOT mark the entire problem correct/incorrect
xpath = "//div[@class='indicator_container']/span[@class='correct']"
self.assert_no_xpath(xml, xpath, self.context)
xpath = "//div[@class='indicator_container']/span[@class='incorrect']"
self.assert_no_xpath(xml, xpath, self.context)
# Should NOT mark individual options
self.assert_no_xpath(xml,
"//label[@class='choicegroup_incorrect']",
self.context)
self.assert_no_xpath(xml,
"//label[@class='choicegroup_correct']",
self.context)
# Expect to see the message
self.assert_has_text(xml, "//div[@class='capa_alert']",
self.context['submitted_message'])
def test_no_message_before_submission(self):
"""Ensure that we don't show the `submitted_message`
before submitting"""
conditions = [
{'input_type': 'radio', 'status': 'unsubmitted', 'value': ''},
{'input_type': 'radio', 'status': 'unsubmitted', 'value': []},
{'input_type': 'checkbox', 'status': 'unsubmitted', 'value': []},
# These tests expose bug #365
# When the bug is fixed, uncomment these cases.
#{'input_type': 'radio', 'status': 'unsubmitted', 'value': '2'},
#{'input_type': 'radio', 'status': 'unsubmitted', 'value': ['2']},
#{'input_type': 'radio', 'status': 'unsubmitted', 'value': '2'},
#{'input_type': 'radio', 'status': 'unsubmitted', 'value': ['2']},
#{'input_type': 'checkbox', 'status': 'unsubmitted', 'value': ['2']},
#{'input_type': 'checkbox', 'status': 'unsubmitted', 'value': ['2']}]
]
self.context['show_correctness'] = 'never'
self.context['submitted_message'] = 'Test message'
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
# Expect that we do NOT see the message yet
self.assert_no_xpath(xml, "//div[@class='capa_alert']", self.context)
class TextlineTemplateTest(TemplateTestCase):
"""Test mako template for `<textline>` input"""
TEMPLATE_NAME = 'textline.html'
def setUp(self):
self.context = {'id': '1',
'status': 'correct',
'value': '3',
'preprocessor': None,
'trailing_text': None}
super(TextlineTemplateTest, self).setUp()
def test_section_class(self):
cases = [({}, ' capa_inputtype '),
({'do_math': True}, 'text-input-dynamath capa_inputtype '),
({'inline': True}, ' capa_inputtype inline'),
({'do_math': True, 'inline': True}, 'text-input-dynamath capa_inputtype inline'), ]
for (context, css_class) in cases:
base_context = self.context.copy()
base_context.update(context)
xml = self.render_to_xml(base_context)
xpath = "//section[@class='%s']" % css_class
self.assert_has_xpath(xml, xpath, self.context)
def test_status(self):
cases = [('correct', 'correct', 'correct'),
('unsubmitted', 'unanswered', 'unanswered'),
('incorrect', 'incorrect', 'incorrect'),
('incomplete', 'incorrect', 'incomplete')]
for (context_status, div_class, status_mark) in cases:
self.context['status'] = context_status
xml = self.render_to_xml(self.context)
# Expect that we get a <div> with correct class
xpath = "//div[@class='%s ']" % div_class
self.assert_has_xpath(xml, xpath, self.context)
# Expect that we get a <p> with class="status"
# (used to by CSS to draw the green check / red x)
self.assert_has_text(xml, "//p[@class='status']",
status_mark, exact=False)
def test_hidden(self):
self.context['hidden'] = True
xml = self.render_to_xml(self.context)
xpath = "//div[@style='display:none;']"
self.assert_has_xpath(xml, xpath, self.context)
xpath = "//input[@style='display:none;']"
self.assert_has_xpath(xml, xpath, self.context)
def test_do_math(self):
self.context['do_math'] = True
xml = self.render_to_xml(self.context)
xpath = "//input[@class='math']"
self.assert_has_xpath(xml, xpath, self.context)
xpath = "//div[@class='equation']"
self.assert_has_xpath(xml, xpath, self.context)
xpath = "//textarea[@id='input_1_dynamath']"
self.assert_has_xpath(xml, xpath, self.context)
def test_size(self):
self.context['size'] = '20'
xml = self.render_to_xml(self.context)
xpath = "//input[@size='20']"
self.assert_has_xpath(xml, xpath, self.context)
def test_preprocessor(self):
self.context['preprocessor'] = {'class_name': 'test_class',
'script_src': 'test_script'}
xml = self.render_to_xml(self.context)
xpath = "//div[@class='text-input-dynamath_data' and @data-preprocessor='test_class']"
self.assert_has_xpath(xml, xpath, self.context)
xpath = "//div[@class='script_placeholder' and @data-src='test_script']"
self.assert_has_xpath(xml, xpath, self.context)
def test_do_inline(self):
cases = [('correct', 'correct'),
('unsubmitted', 'unanswered'),
('incorrect', 'incorrect'),
('incomplete', 'incorrect')]
self.context['inline'] = True
for (context_status, div_class) in cases:
self.context['status'] = context_status
xml = self.render_to_xml(self.context)
# Expect that we get a <div> with correct class
xpath = "//div[@class='%s inline']" % div_class
self.assert_has_xpath(xml, xpath, self.context)
def test_message(self):
self.context['msg'] = "Test message"
xml = self.render_to_xml(self.context)
xpath = "//span[@class='message']"
self.assert_has_text(xml, xpath, self.context['msg'])

View File

@@ -60,6 +60,7 @@ class OptionInputTest(unittest.TestCase):
def test_option_parsing(self):
f = inputtypes.OptionInput.parse_options
def check(input, options):
"""Take list of options, confirm that output is in the silly doubled format"""
expected = [(o, o) for o in options]
@@ -120,7 +121,6 @@ class ChoiceGroupTest(unittest.TestCase):
self.check_group('checkboxgroup', 'checkbox', '[]')
class JavascriptInputTest(unittest.TestCase):
'''
The javascript input is a pretty straightforward pass-thru, but test it anyway
@@ -182,10 +182,10 @@ class TextLineTest(unittest.TestCase):
'hidden': False,
'inline': False,
'do_math': False,
'trailing_text': '',
'preprocessor': None}
self.assertEqual(context, expected)
def test_math_rendering(self):
size = "42"
preprocessorClass = "preParty"
@@ -209,11 +209,49 @@ class TextLineTest(unittest.TestCase):
'msg': '',
'hidden': False,
'inline': False,
'trailing_text': '',
'do_math': True,
'preprocessor': {'class_name': preprocessorClass,
'script_src': script}}
self.assertEqual(context, expected)
def test_trailing_text_rendering(self):
size = "42"
# store (xml_text, expected)
trailing_text = []
# standard trailing text
trailing_text.append(('m/s', 'm/s'))
# unicode trailing text
trailing_text.append((u'\xc3', u'\xc3'))
# html escaped trailing text
# this is the only one we expect to change
trailing_text.append(('a &lt; b', 'a < b'))
for xml_text, expected_text in trailing_text:
xml_str = u"""<textline id="prob_1_2"
size="{size}"
trailing_text="{tt}"
/>""".format(size=size, tt=xml_text)
element = etree.fromstring(xml_str)
state = {'value': 'BumbleBee', }
the_input = lookup_tag('textline')(test_system, element, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': 'BumbleBee',
'status': 'unanswered',
'size': size,
'msg': '',
'hidden': False,
'inline': False,
'do_math': False,
'trailing_text': expected_text,
'preprocessor': None}
self.assertEqual(context, expected)
class FileSubmissionTest(unittest.TestCase):
'''
@@ -230,7 +268,6 @@ class FileSubmissionTest(unittest.TestCase):
/>""".format(af=allowed_files,
rf=required_files,)
element = etree.fromstring(xml_str)
state = {'value': 'BumbleBee.py',
@@ -242,12 +279,12 @@ class FileSubmissionTest(unittest.TestCase):
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'status': 'queued',
'msg': input_class.submitted_msg,
'value': 'BumbleBee.py',
'queue_len': '3',
'allowed_files': '["runme.py", "nooooo.rb", "ohai.java"]',
'required_files': '["cookies.py"]'}
'status': 'queued',
'msg': input_class.submitted_msg,
'value': 'BumbleBee.py',
'queue_len': '3',
'allowed_files': '["runme.py", "nooooo.rb", "ohai.java"]',
'required_files': '["cookies.py"]'}
self.assertEqual(context, expected)
@@ -288,19 +325,19 @@ class CodeInputTest(unittest.TestCase):
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
'status': 'queued',
'msg': input_class.submitted_msg,
'mode': mode,
'linenumbers': linenumbers,
'rows': rows,
'cols': cols,
'hidden': '',
'tabsize': int(tabsize),
'queue_len': '3',
}
'status': 'queued',
'msg': input_class.submitted_msg,
'mode': mode,
'linenumbers': linenumbers,
'rows': rows,
'cols': cols,
'hidden': '',
'tabsize': int(tabsize),
'queue_len': '3'}
self.assertEqual(context, expected)
class MatlabTest(unittest.TestCase):
'''
Test Matlab input types
@@ -313,18 +350,18 @@ class MatlabTest(unittest.TestCase):
self.payload = "payload"
self.linenumbers = 'true'
self.xml = """<matlabinput id="prob_1_2"
rows="{r}" cols="{c}"
rows="{r}" cols="{c}"
tabsize="{tabsize}" mode="{m}"
linenumbers="{ln}">
<plot_payload>
{payload}
</plot_payload>
</matlabinput>""".format(r = self.rows,
c = self.cols,
tabsize = self.tabsize,
m = self.mode,
payload = self.payload,
ln = self.linenumbers)
</matlabinput>""".format(r=self.rows,
c=self.cols,
tabsize=self.tabsize,
m=self.mode,
payload=self.payload,
ln=self.linenumbers)
elt = etree.fromstring(self.xml)
state = {'value': 'print "good evening"',
'status': 'incomplete',
@@ -333,27 +370,24 @@ class MatlabTest(unittest.TestCase):
self.input_class = lookup_tag('matlabinput')
self.the_input = self.input_class(test_system, elt, state)
def test_rendering(self):
context = self.the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
'status': 'queued',
'msg': self.input_class.submitted_msg,
'mode': self.mode,
'rows': self.rows,
'cols': self.cols,
'queue_msg': '',
'linenumbers': 'true',
'hidden': '',
'tabsize': int(self.tabsize),
'queue_len': '3',
}
'status': 'queued',
'msg': self.input_class.submitted_msg,
'mode': self.mode,
'rows': self.rows,
'cols': self.cols,
'queue_msg': '',
'linenumbers': 'true',
'hidden': '',
'tabsize': int(self.tabsize),
'queue_len': '3'}
self.assertEqual(context, expected)
def test_rendering_with_state(self):
state = {'value': 'print "good evening"',
'status': 'incomplete',
@@ -366,17 +400,16 @@ class MatlabTest(unittest.TestCase):
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
'status': 'queued',
'msg': self.input_class.submitted_msg,
'mode': self.mode,
'rows': self.rows,
'cols': self.cols,
'queue_msg': 'message',
'linenumbers': 'true',
'hidden': '',
'tabsize': int(self.tabsize),
'queue_len': '3',
}
'status': 'queued',
'msg': self.input_class.submitted_msg,
'mode': self.mode,
'rows': self.rows,
'cols': self.cols,
'queue_msg': 'message',
'linenumbers': 'true',
'hidden': '',
'tabsize': int(self.tabsize),
'queue_len': '3'}
self.assertEqual(context, expected)
@@ -391,17 +424,16 @@ class MatlabTest(unittest.TestCase):
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
'status': 'queued',
'msg': self.input_class.plot_submitted_msg,
'mode': self.mode,
'rows': self.rows,
'cols': self.cols,
'queue_msg': '',
'linenumbers': 'true',
'hidden': '',
'tabsize': int(self.tabsize),
'queue_len': '1',
}
'status': 'queued',
'msg': self.input_class.plot_submitted_msg,
'mode': self.mode,
'rows': self.rows,
'cols': self.cols,
'queue_msg': '',
'linenumbers': 'true',
'hidden': '',
'tabsize': int(self.tabsize),
'queue_len': '1'}
self.assertEqual(context, expected)
@@ -410,7 +442,7 @@ class MatlabTest(unittest.TestCase):
response = self.the_input.handle_ajax("plot", get)
test_system.xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY)
self.assertTrue(response['success'])
self.assertTrue(self.the_input.input_state['queuekey'] is not None)
self.assertEqual(self.the_input.input_state['queuestate'], 'queued')
@@ -452,9 +484,6 @@ class MatlabTest(unittest.TestCase):
self.assertFalse('queue_msg' in input_state)
class SchematicTest(unittest.TestCase):
'''
Check that schematic inputs work
@@ -468,7 +497,6 @@ class SchematicTest(unittest.TestCase):
initial_value = 'two large batteries'
submit_analyses = 'maybe'
xml_str = """<schematic id="prob_1_2"
height="{h}"
width="{w}"
@@ -498,8 +526,7 @@ class SchematicTest(unittest.TestCase):
'height': height,
'parts': parts,
'analyses': analyses,
'submit_analyses': submit_analyses,
}
'submit_analyses': submit_analyses}
self.assertEqual(context, expected)

View File

@@ -50,9 +50,7 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
correct_answer = [
{
'draggables': ['p'],
'targets': [
'p_l', 'p_r'
],
'targets': ['p_l', 'p_r'],
'rule': 'anyof'
},
{
@@ -91,18 +89,18 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
'rule': 'unordered_equal'
},
{
'draggables': ['up_and_down'],
'targets': [
'draggables': ['up_and_down'],
'targets': [
's_l[s][1]', 's_r[s][1]'
],
'rule': 'unordered_equal'
],
'rule': 'unordered_equal'
},
{
'draggables': ['up'],
'targets': [
'draggables': ['up'],
'targets': [
'p_l[p][1]', 'p_l[p][3]', 'p_r[p][1]', 'p_r[p][3]'
],
'rule': 'unordered_equal'
],
'rule': 'unordered_equal'
}
]
self.assertTrue(draganddrop.grade(user_input, correct_answer))
@@ -133,18 +131,18 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
'rule': 'unordered_equal'
},
{
'draggables': ['up_and_down'],
'targets': [
'draggables': ['up_and_down'],
'targets': [
's_l[s][1]', 's_r[s][1]'
],
'rule': 'unordered_equal'
],
'rule': 'unordered_equal'
},
{
'draggables': ['up'],
'targets': [
'draggables': ['up'],
'targets': [
'p_l[p][1]', 'p_l[p][3]', 'p_r[p][1]', 'p_r[p][3]'
],
'rule': 'unordered_equal'
],
'rule': 'unordered_equal'
}
]
self.assertTrue(draganddrop.grade(user_input, correct_answer))
@@ -188,22 +186,21 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
'rule': 'unordered_equal'
},
{
'draggables': ['up_and_down'],
'targets': [
'left_side_tagret[molecule][s_target][s][1]',
'right_side_tagret[molecule][s_target][s][1]'
],
'rule': 'unordered_equal'
'draggables': ['up_and_down'],
'targets': [
'left_side_tagret[molecule][s_target][s][1]',
'right_side_tagret[molecule][s_target][s][1]'
],
'rule': 'unordered_equal'
},
{
'draggables': ['up'],
'targets': [
'left_side_tagret[molecule][p_target][p][1]',
'left_side_tagret[molecule][p_target][p][3]',
'right_side_tagret[molecule][p_target][p][1]',
'right_side_tagret[molecule][p_target][p][3]'
],
'rule': 'unordered_equal'
'draggables': ['up'],
'targets': [
'left_side_tagret[molecule][p_target][p][1]',
'left_side_tagret[molecule][p_target][p][3]',
'right_side_tagret[molecule][p_target][p][1]',
'right_side_tagret[molecule][p_target][p][3]'],
'rule': 'unordered_equal'
}
]
self.assertTrue(draganddrop.grade(user_input, correct_answer))
@@ -242,32 +239,32 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
# up, up_and_down
correct_answer = [
{
'draggables': ['triple_draggable'],
'targets': ['p_l', 'p_r'],
'rule': 'unordered_equal'
'draggables': ['triple_draggable'],
'targets': ['p_l', 'p_r'],
'rule': 'unordered_equal'
},
{
'draggables': ['double_draggable'],
'targets': ['p_pi', 'p_pi*'],
'rule': 'unordered_equal'
'draggables': ['double_draggable'],
'targets': ['p_pi', 'p_pi*'],
'rule': 'unordered_equal'
},
{
'draggables': ['single_draggable'],
'targets': ['s_l', 's_r', 's_sigma', 's_sigma*', 'p_sigma', 'p_sigma*'],
'rule': 'unordered_equal'
'draggables': ['single_draggable'],
'targets': ['s_l', 's_r', 's_sigma', 's_sigma*', 'p_sigma', 'p_sigma*'],
'rule': 'unordered_equal'
},
{
'draggables': ['up'],
'targets': ['p_l[triple_draggable][1]', 'p_l[triple_draggable][2]',
'p_r[triple_draggable][2]', 'p_r[triple_draggable][3]'],
'rule': 'unordered_equal'
'draggables': ['up'],
'targets': ['p_l[triple_draggable][1]', 'p_l[triple_draggable][2]',
'p_r[triple_draggable][2]', 'p_r[triple_draggable][3]'],
'rule': 'unordered_equal'
},
{
'draggables': ['up_and_down'],
'targets': ['s_l[single_draggable][1]', 's_r[single_draggable][1]',
's_sigma[single_draggable][1]', 's_sigma*[single_draggable][1]',
'p_pi[double_draggable][1]', 'p_pi[double_draggable][2]'],
'rule': 'unordered_equal'
'draggables': ['up_and_down'],
'targets': ['s_l[single_draggable][1]', 's_r[single_draggable][1]',
's_sigma[single_draggable][1]', 's_sigma*[single_draggable][1]',
'p_pi[double_draggable][1]', 'p_pi[double_draggable][2]'],
'rule': 'unordered_equal'
},
]
@@ -290,7 +287,6 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
correct_answer = []
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_targets_false(self):
user_input = '[{"1": "t1"}, \
{"name_with_icon": "t2"}]'
@@ -300,46 +296,44 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
def test_multiple_images_per_target_true(self):
user_input = '[{"1": "t1"}, {"name_with_icon": "t2"}, \
{"2": "t1"}]'
correct_answer = {'1': 't1', 'name_with_icon': 't2',
'2': 't1'}
correct_answer = {'1': 't1', 'name_with_icon': 't2', '2': 't1'}
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_multiple_images_per_target_false(self):
user_input = '[{"1": "t1"}, {"name_with_icon": "t2"}, \
{"2": "t1"}]'
correct_answer = {'1': 't2', 'name_with_icon': 't2',
'2': 't1'}
correct_answer = {'1': 't2', 'name_with_icon': 't2', '2': 't1'}
self.assertFalse(draganddrop.grade(user_input, correct_answer))
def test_targets_and_positions(self):
user_input = '[{"1": [10,10]}, \
{"name_with_icon": [[10,10],4]}]'
correct_answer = {'1': [10, 10], 'name_with_icon': [[10, 10], 4]}
correct_answer = {'1': [10, 10], 'name_with_icon': [[10, 10], 4]}
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_position_and_targets(self):
user_input = '[{"1": "t1"}, {"name_with_icon": "t2"}]'
correct_answer = {'1': 't1', 'name_with_icon': 't2'}
correct_answer = {'1': 't1', 'name_with_icon': 't2'}
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_positions_exact(self):
user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]'
correct_answer = {'1': [10, 10], 'name_with_icon': [20, 20]}
correct_answer = {'1': [10, 10], 'name_with_icon': [20, 20]}
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_positions_false(self):
user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]'
correct_answer = {'1': [25, 25], 'name_with_icon': [20, 20]}
correct_answer = {'1': [25, 25], 'name_with_icon': [20, 20]}
self.assertFalse(draganddrop.grade(user_input, correct_answer))
def test_positions_true_in_radius(self):
user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]'
correct_answer = {'1': [14, 14], 'name_with_icon': [20, 20]}
correct_answer = {'1': [14, 14], 'name_with_icon': [20, 20]}
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_positions_true_in_manual_radius(self):
user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]'
correct_answer = {'1': [[40, 10], 30], 'name_with_icon': [20, 20]}
correct_answer = {'1': [[40, 10], 30], 'name_with_icon': [20, 20]}
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_positions_false_in_manual_radius(self):
@@ -349,7 +343,7 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
def test_correct_answer_not_has_key_from_user_answer(self):
user_input = '[{"1": "t1"}, {"name_with_icon": "t2"}]'
correct_answer = {'3': 't3', 'name_with_icon': 't2'}
correct_answer = {'3': 't3', 'name_with_icon': 't2'}
self.assertFalse(draganddrop.grade(user_input, correct_answer))
def test_anywhere(self):
@@ -359,7 +353,7 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
user_input = '[{"ant":[610.5,57.449951171875]},\
{"grass":[322.5,199.449951171875]}]'
correct_answer = {'grass': [[300, 200], 200], 'ant': [[500, 0], 200]}
correct_answer = {'grass': [[300, 200], 200], 'ant': [[500, 0], 200]}
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_lcao_correct(self):
@@ -372,10 +366,10 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
{"12":"p_sigma_name"},{"14":"p_sigma_star_name"}]'
correct_answer = [{
'draggables': ['1', '2', '3', '4', '5', '6'],
'targets': [
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'
],
'draggables': ['1', '2', '3', '4', '5', '6'],
'targets': [
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'
],
'rule': 'anyof'
}, {
'draggables': ['7', '8', '9', '10'],
@@ -411,9 +405,9 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
{"12":"p_sigma_name"},{"14":"p_sigma_star_name"}]'
correct_answer = [{
'draggables': ['1', '2', '3', '4', '5', '6'],
'targets': [
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'
'draggables': ['1', '2', '3', '4', '5', '6'],
'targets': [
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'
],
'rule': 'anyof'
}, {
@@ -446,21 +440,22 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
{"2":"target2"},{"1":"target3"},{"2":"target4"},{"2":"target5"}, \
{"3":"target6"}]'
correct_answer = [
{
'draggables': ['1'],
'targets': ['target1', 'target3'],
'rule': 'anyof'
},
{
'draggables': ['2'],
'targets': ['target2', 'target4', 'target5'],
'rule': 'anyof'
},
{
'draggables': ['3'],
'targets': ['target6'],
'rule': 'anyof'
}]
{
'draggables': ['1'],
'targets': ['target1', 'target3'],
'rule': 'anyof'
},
{
'draggables': ['2'],
'targets': ['target2', 'target4', 'target5'],
'rule': 'anyof'
},
{
'draggables': ['3'],
'targets': ['target6'],
'rule': 'anyof'
}
]
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_reuse_draggable_with_mupliples(self):
@@ -469,21 +464,22 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
{"2":"target2"},{"1":"target1"},{"2":"target4"},{"2":"target4"}, \
{"3":"target6"}]'
correct_answer = [
{
'draggables': ['1'],
'targets': ['target1', 'target3'],
'rule': 'anyof'
},
{
'draggables': ['2'],
'targets': ['target2', 'target4'],
'rule': 'anyof'
},
{
'draggables': ['3'],
'targets': ['target6'],
'rule': 'anyof'
}]
{
'draggables': ['1'],
'targets': ['target1', 'target3'],
'rule': 'anyof'
},
{
'draggables': ['2'],
'targets': ['target2', 'target4'],
'rule': 'anyof'
},
{
'draggables': ['3'],
'targets': ['target6'],
'rule': 'anyof'
}
]
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_reuse_many_draggable_with_mupliples(self):
@@ -493,26 +489,27 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
{"3":"target6"}, {"4": "target3"}, {"5": "target4"}, \
{"5": "target5"}, {"6": "target2"}]'
correct_answer = [
{
'draggables': ['1', '4'],
'targets': ['target1', 'target3'],
'rule': 'anyof'
},
{
'draggables': ['2', '6'],
'targets': ['target2', 'target4'],
'rule': 'anyof'
},
{
'draggables': ['5'],
'targets': ['target4', 'target5'],
'rule': 'anyof'
},
{
'draggables': ['3'],
'targets': ['target6'],
'rule': 'anyof'
}]
{
'draggables': ['1', '4'],
'targets': ['target1', 'target3'],
'rule': 'anyof'
},
{
'draggables': ['2', '6'],
'targets': ['target2', 'target4'],
'rule': 'anyof'
},
{
'draggables': ['5'],
'targets': ['target4', 'target5'],
'rule': 'anyof'
},
{
'draggables': ['3'],
'targets': ['target6'],
'rule': 'anyof'
}
]
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_reuse_many_draggable_with_mupliples_wrong(self):
@@ -524,26 +521,26 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
{"3":"target6"}, {"4": "target3"}, {"5": "target4"}, \
{"5": "target5"}, {"6": "target2"}]'
correct_answer = [
{
'draggables': ['1', '4'],
'targets': ['target1', 'target3'],
'rule': 'anyof'
},
{
'draggables': ['2', '6'],
'targets': ['target2', 'target4'],
'rule': 'anyof'
},
{
'draggables': ['5'],
'targets': ['target4', 'target5'],
'rule': 'anyof'
},
{
'draggables': ['3'],
'targets': ['target6'],
'rule': 'anyof'
}]
{
'draggables': ['1', '4'],
'targets': ['target1', 'target3'],
'rule': 'anyof'
},
{
'draggables': ['2', '6'],
'targets': ['target2', 'target4'],
'rule': 'anyof'
},
{
'draggables': ['5'],
'targets': ['target4', 'target5'],
'rule': 'anyof'
},
{
'draggables': ['3'],
'targets': ['target6'],
'rule': 'anyof'
}]
self.assertFalse(draganddrop.grade(user_input, correct_answer))
def test_label_10_targets_with_a_b_c_false(self):
@@ -553,21 +550,22 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
{"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \
{"a":"target1"}]'
correct_answer = [
{
'draggables': ['a'],
'targets': ['target1', 'target4', 'target7', 'target10'],
'rule': 'unordered_equal'
},
{
'draggables': ['b'],
'targets': ['target2', 'target5', 'target8'],
'rule': 'unordered_equal'
},
{
'draggables': ['c'],
'targets': ['target3', 'target6', 'target9'],
'rule': 'unordered_equal'
}]
{
'draggables': ['a'],
'targets': ['target1', 'target4', 'target7', 'target10'],
'rule': 'unordered_equal'
},
{
'draggables': ['b'],
'targets': ['target2', 'target5', 'target8'],
'rule': 'unordered_equal'
},
{
'draggables': ['c'],
'targets': ['target3', 'target6', 'target9'],
'rule': 'unordered_equal'
}
]
self.assertFalse(draganddrop.grade(user_input, correct_answer))
def test_label_10_targets_with_a_b_c_(self):
@@ -577,21 +575,22 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
{"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \
{"a":"target10"}]'
correct_answer = [
{
'draggables': ['a'],
'targets': ['target1', 'target4', 'target7', 'target10'],
'rule': 'unordered_equal'
},
{
'draggables': ['b'],
'targets': ['target2', 'target5', 'target8'],
'rule': 'unordered_equal'
},
{
'draggables': ['c'],
'targets': ['target3', 'target6', 'target9'],
'rule': 'unordered_equal'
}]
{
'draggables': ['a'],
'targets': ['target1', 'target4', 'target7', 'target10'],
'rule': 'unordered_equal'
},
{
'draggables': ['b'],
'targets': ['target2', 'target5', 'target8'],
'rule': 'unordered_equal'
},
{
'draggables': ['c'],
'targets': ['target3', 'target6', 'target9'],
'rule': 'unordered_equal'
}
]
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_label_10_targets_with_a_b_c_multiple(self):
@@ -601,21 +600,22 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
{"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \
{"a":"target1"}]'
correct_answer = [
{
'draggables': ['a', 'a', 'a'],
'targets': ['target1', 'target4', 'target7', 'target10'],
'rule': 'anyof+number'
},
{
'draggables': ['b', 'b', 'b'],
'targets': ['target2', 'target5', 'target8'],
'rule': 'anyof+number'
},
{
'draggables': ['c', 'c', 'c'],
'targets': ['target3', 'target6', 'target9'],
'rule': 'anyof+number'
}]
{
'draggables': ['a', 'a', 'a'],
'targets': ['target1', 'target4', 'target7', 'target10'],
'rule': 'anyof+number'
},
{
'draggables': ['b', 'b', 'b'],
'targets': ['target2', 'target5', 'target8'],
'rule': 'anyof+number'
},
{
'draggables': ['c', 'c', 'c'],
'targets': ['target3', 'target6', 'target9'],
'rule': 'anyof+number'
}
]
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_label_10_targets_with_a_b_c_multiple_false(self):
@@ -625,21 +625,22 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
{"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \
{"a":"target1"}]'
correct_answer = [
{
'draggables': ['a', 'a', 'a'],
'targets': ['target1', 'target4', 'target7', 'target10'],
'rule': 'anyof+number'
},
{
'draggables': ['b', 'b', 'b'],
'targets': ['target2', 'target5', 'target8'],
'rule': 'anyof+number'
},
{
'draggables': ['c', 'c', 'c'],
'targets': ['target3', 'target6', 'target9'],
'rule': 'anyof+number'
}]
{
'draggables': ['a', 'a', 'a'],
'targets': ['target1', 'target4', 'target7', 'target10'],
'rule': 'anyof+number'
},
{
'draggables': ['b', 'b', 'b'],
'targets': ['target2', 'target5', 'target8'],
'rule': 'anyof+number'
},
{
'draggables': ['c', 'c', 'c'],
'targets': ['target3', 'target6', 'target9'],
'rule': 'anyof+number'
}
]
self.assertFalse(draganddrop.grade(user_input, correct_answer))
def test_label_10_targets_with_a_b_c_reused(self):
@@ -649,21 +650,22 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
{"c":"target6"}, {"b":"target8"},{"c":"target9"}, \
{"a":"target10"}]'
correct_answer = [
{
'draggables': ['a', 'a'],
'targets': ['target1', 'target10'],
'rule': 'unordered_equal+number'
},
{
'draggables': ['b', 'b', 'b'],
'targets': ['target2', 'target5', 'target8'],
'rule': 'unordered_equal+number'
},
{
'draggables': ['c', 'c', 'c'],
'targets': ['target3', 'target6', 'target9'],
'rule': 'unordered_equal+number'
}]
{
'draggables': ['a', 'a'],
'targets': ['target1', 'target10'],
'rule': 'unordered_equal+number'
},
{
'draggables': ['b', 'b', 'b'],
'targets': ['target2', 'target5', 'target8'],
'rule': 'unordered_equal+number'
},
{
'draggables': ['c', 'c', 'c'],
'targets': ['target3', 'target6', 'target9'],
'rule': 'unordered_equal+number'
}
]
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_label_10_targets_with_a_b_c_reused_false(self):
@@ -673,21 +675,22 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
{"c":"target6"}, {"b":"target8"},{"c":"target9"}, \
{"a":"target10"}]'
correct_answer = [
{
'draggables': ['a', 'a'],
'targets': ['target1', 'target10'],
'rule': 'unordered_equal+number'
},
{
'draggables': ['b', 'b', 'b'],
'targets': ['target2', 'target5', 'target8'],
'rule': 'unordered_equal+number'
},
{
'draggables': ['c', 'c', 'c'],
'targets': ['target3', 'target6', 'target9'],
'rule': 'unordered_equal+number'
}]
{
'draggables': ['a', 'a'],
'targets': ['target1', 'target10'],
'rule': 'unordered_equal+number'
},
{
'draggables': ['b', 'b', 'b'],
'targets': ['target2', 'target5', 'target8'],
'rule': 'unordered_equal+number'
},
{
'draggables': ['c', 'c', 'c'],
'targets': ['target3', 'target6', 'target9'],
'rule': 'unordered_equal+number'
}
]
self.assertFalse(draganddrop.grade(user_input, correct_answer))
def test_mixed_reuse_and_not_reuse(self):
@@ -696,16 +699,17 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
{"b":"target2"},{"c":"target3"}, {"a":"target4"},\
{"a":"target5"}]'
correct_answer = [
{
'draggables': ['a', 'b'],
'targets': ['target1', 'target2', 'target4', 'target5'],
'rule': 'anyof'
},
{
'draggables': ['c'],
'targets': ['target3'],
'rule': 'exact'
}]
{
'draggables': ['a', 'b'],
'targets': ['target1', 'target2', 'target4', 'target5'],
'rule': 'anyof'
},
{
'draggables': ['c'],
'targets': ['target3'],
'rule': 'exact'
}
]
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_mixed_reuse_and_not_reuse_number(self):
@@ -713,16 +717,17 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
user_input = '[{"a":"target1"}, \
{"b":"target2"},{"c":"target3"}, {"a":"target4"}]'
correct_answer = [
{
'draggables': ['a', 'a', 'b'],
'targets': ['target1', 'target2', 'target4'],
'rule': 'anyof+number'
},
{
'draggables': ['c'],
'targets': ['target3'],
'rule': 'exact'
}]
{
'draggables': ['a', 'a', 'b'],
'targets': ['target1', 'target2', 'target4'],
'rule': 'anyof+number'
},
{
'draggables': ['c'],
'targets': ['target3'],
'rule': 'exact'
}
]
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_mixed_reuse_and_not_reuse_number_false(self):
@@ -730,16 +735,17 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
user_input = '[{"a":"target1"}, \
{"b":"target2"},{"c":"target3"}, {"a":"target4"}, {"a":"target10"}]'
correct_answer = [
{
'draggables': ['a', 'a', 'b'],
'targets': ['target1', 'target2', 'target4', 'target10'],
'rule': 'anyof_number'
},
{
'draggables': ['c'],
'targets': ['target3'],
'rule': 'exact'
}]
{
'draggables': ['a', 'a', 'b'],
'targets': ['target1', 'target2', 'target4', 'target10'],
'rule': 'anyof_number'
},
{
'draggables': ['c'],
'targets': ['target3'],
'rule': 'exact'
}
]
self.assertFalse(draganddrop.grade(user_input, correct_answer))
def test_alternative_correct_answer(self):
@@ -747,9 +753,9 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
{"name_with_icon":"t1"},{"name_with_icon":"t1"},{"name4":"t1"}, \
{"name4":"t1"}]'
correct_answer = [
{'draggables': ['name4'], 'targets': ['t1', 't1'], 'rule': 'exact'},
{'draggables': ['name_with_icon'], 'targets': ['t1', 't1', 't1'],
'rule': 'exact'}
{'draggables': ['name4'], 'targets': ['t1', 't1'], 'rule': 'exact'},
{'draggables': ['name_with_icon'], 'targets': ['t1', 't1', 't1'],
'rule': 'exact'}
]
self.assertTrue(draganddrop.grade(user_input, correct_answer))
@@ -783,8 +789,8 @@ class Test_DraAndDrop_Compare_Positions(unittest.TestCase):
def test_2a(self):
dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]')
self.assertTrue(dnd.compare_positions(correct=[[1, 1], [2, 3]],
user=[[2, 3], [1, 1]],
flag='exact'))
user=[[2, 3], [1, 1]],
flag='exact'))
def test_2b(self):
dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]')
@@ -813,8 +819,8 @@ class Test_DraAndDrop_Compare_Positions(unittest.TestCase):
def test_6(self):
dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]')
self.assertTrue(dnd.compare_positions(correct=["a", "b", "c"],
user=["a", "c", "b"],
flag='anyof'))
user=["a", "c", "b"],
flag='anyof'))
def test_7(self):
dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]')
@@ -826,10 +832,9 @@ class Test_DraAndDrop_Compare_Positions(unittest.TestCase):
def suite():
testcases = [Test_PositionsCompare,
Test_DragAndDrop_Populate,
Test_DragAndDrop_Grade,
Test_DraAndDrop_Compare_Positions
]
Test_DragAndDrop_Populate,
Test_DragAndDrop_Grade,
Test_DraAndDrop_Compare_Positions]
suites = []
for testcase in testcases:
suites.append(unittest.TestLoader().loadTestsFromTestCase(testcase))

View File

@@ -6,7 +6,7 @@ from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlDescriptor
from xmodule.exceptions import InvalidDefinitionError
from xblock.core import String, Scope, Object, BlockScope
from xblock.core import String, Scope, Object
DEFAULT = "_DEFAULT_GROUP"

View File

@@ -1,11 +1,10 @@
import logging
from lxml import etree
from pkg_resources import resource_string, resource_listdir
from pkg_resources import resource_string
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.contentstore.content import StaticContent
from xblock.core import Scope, String
log = logging.getLogger(__name__)
@@ -25,7 +24,6 @@ class AnnotatableModule(AnnotatableFields, XModule):
css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
icon_class = 'annotatable'
def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)

View File

@@ -99,6 +99,7 @@ class CapaFields(object):
seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state)
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
markdown = String(help="Markdown source of this module", scope=Scope.settings)
source_code = String(help="Source code for LaTeX and Word problems. This feature is not well-supported.", scope=Scope.settings)
class CapaModule(CapaFields, XModule):

View File

@@ -14,7 +14,7 @@ from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
log = logging.getLogger("mitx.courseware")
V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload",
"skip_spelling_checks", "due", "graceperiod"]
"skip_spelling_checks", "due", "graceperiod", "weight"]
V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state",
"student_attempts", "ready_to_reset"]
@@ -219,5 +219,5 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
stores_state = True
has_score = True
always_recalculate_grades=True
always_recalculate_grades = True
template_dir_name = "combinedopenended"

View File

@@ -10,7 +10,7 @@ from pkg_resources import resource_string
from xmodule.x_module import XModule
from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor
from xblock.core import String, Scope, List
from xblock.core import Scope, List
from xmodule.modulestore.exceptions import ItemNotFoundError
@@ -60,8 +60,7 @@ class ConditionalModule(ConditionalFields, XModule):
js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'),
resource_string(__name__, 'js/src/conditional/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
]}
]}
js_module_name = "Conditional"
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
@@ -82,21 +81,24 @@ class ConditionalModule(ConditionalFields, XModule):
xml_value = self.descriptor.xml_attributes.get(xml_attr)
if xml_value:
return xml_value, attr_name
raise Exception('Error in conditional module: unknown condition "%s"'
% xml_attr)
raise Exception('Error in conditional module: unknown condition "%s"' % xml_attr)
def is_condition_satisfied(self):
self.required_modules = [self.system.get_module(descriptor) for
descriptor in self.descriptor.get_required_module_descriptors()]
descriptor in self.descriptor.get_required_module_descriptors()]
xml_value, attr_name = self._get_condition()
if xml_value and self.required_modules:
for module in self.required_modules:
if not hasattr(module, attr_name):
raise Exception('Error in conditional module: \
required module {module} has no {module_attr}'.format(
module=module, module_attr=attr_name))
# We don't throw an exception here because it is possible for
# the descriptor of a required module to have a property but
# for the resulting module to be a (flavor of) ErrorModule.
# So just log and return false.
log.warn('Error in conditional module: \
required module {module} has no {module_attr}'.format(module=module, module_attr=attr_name))
return False
attr = getattr(module, attr_name)
if callable(attr):
@@ -111,7 +113,7 @@ class ConditionalModule(ConditionalFields, XModule):
def get_html(self):
# Calculate html ids of dependencies
self.required_html_ids = [descriptor.location.html_id() for
descriptor in self.descriptor.get_required_module_descriptors()]
descriptor in self.descriptor.get_required_module_descriptors()]
return self.system.render_template('conditional_ajax.html', {
'element_id': self.location.html_id(),
@@ -130,7 +132,7 @@ class ConditionalModule(ConditionalFields, XModule):
context = {'module': self,
'message': message}
html = self.system.render_template('conditional_module.html',
context)
context)
return json.dumps({'html': [html], 'message': bool(message)})
html = [child.get_html() for child in self.get_display_items()]
@@ -139,16 +141,15 @@ class ConditionalModule(ConditionalFields, XModule):
def get_icon_class(self):
new_class = 'other'
if self.is_condition_satisfied():
# HACK: This shouldn't be hard-coded to two types
# OBSOLETE: This obsoletes 'type'
class_priority = ['video', 'problem']
# HACK: This shouldn't be hard-coded to two types
# OBSOLETE: This obsoletes 'type'
class_priority = ['video', 'problem']
child_classes = [self.system.get_module(child_descriptor).get_icon_class()
for child_descriptor in self.descriptor.get_children()]
for c in class_priority:
if c in child_classes:
new_class = c
child_classes = [self.system.get_module(child_descriptor).get_icon_class()
for child_descriptor in self.descriptor.get_children()]
for c in class_priority:
if c in child_classes:
new_class = c
return new_class
@@ -163,7 +164,6 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor):
stores_state = True
has_score = False
@staticmethod
def parse_sources(xml_element, system, return_descriptor=False):
"""Parse xml_element 'sources' attr and:

View File

@@ -9,6 +9,7 @@ import StringIO
from xmodule.modulestore import Location
from .django import contentstore
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
from PIL import Image
@@ -59,8 +60,9 @@ class StaticContent(object):
@staticmethod
def get_id_from_location(location):
return {'tag': location.tag, 'org': location.org, 'course': location.course,
'category': location.category, 'name': location.name,
'revision': location.revision}
'category': location.category, 'name': location.name,
'revision': location.revision}
@staticmethod
def get_location_from_path(path):
# remove leading / character if it is there one
@@ -79,8 +81,6 @@ class StaticContent(object):
return StaticContent.get_url_path_from_location(loc)
class ContentStore(object):
'''
Abstraction for all ContentStore providers (e.g. MongoDB)
@@ -119,7 +119,7 @@ class ContentStore(object):
thumbnail_name = StaticContent.generate_thumbnail_name(content.location.name)
thumbnail_file_location = StaticContent.compute_location(content.location.org, content.location.course,
thumbnail_name, is_thumbnail=True)
thumbnail_name, is_thumbnail=True)
# if we're uploading an image, then let's generate a thumbnail so that we can
# serve it up when needed without having to rescale on the fly

View File

@@ -1,6 +1,5 @@
from __future__ import absolute_import
from importlib import import_module
from os import environ
from django.conf import settings

View File

@@ -6,7 +6,6 @@ from gridfs.errors import NoFile
from xmodule.modulestore.mongo import location_to_query, Location
from xmodule.contentstore.content import XASSET_LOCATION_TAG
import sys
import logging
from .content import StaticContent, ContentStore
@@ -26,7 +25,6 @@ class MongoContentStore(ContentStore):
self.fs = gridfs.GridFS(_db)
self.fs_files = _db["fs.files"] # the underlying collection GridFS uses
def save(self, content):
id = content.get_id()
@@ -34,7 +32,8 @@ class MongoContentStore(ContentStore):
self.delete(id)
with self.fs.new_file(_id=id, filename=content.get_url_path(), content_type=content.content_type,
displayname=content.name, thumbnail_location=content.thumbnail_location, import_path=content.import_path) as fp:
displayname=content.name, thumbnail_location=content.thumbnail_location,
import_path=content.import_path) as fp:
fp.write(content.data)
@@ -49,8 +48,9 @@ class MongoContentStore(ContentStore):
try:
with self.fs.get(id) as fp:
return StaticContent(location, fp.displayname, fp.content_type, fp.read(),
fp.uploadDate, thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
import_path=fp.import_path if hasattr(fp, 'import_path') else None)
fp.uploadDate,
thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
import_path=fp.import_path if hasattr(fp, 'import_path') else None)
except NoFile:
raise NotFoundError()
@@ -102,7 +102,7 @@ class MongoContentStore(ContentStore):
]
'''
course_filter = Location(XASSET_LOCATION_TAG, category="asset" if not get_thumbnails else "thumbnail",
course=location.course, org=location.org)
course=location.course, org=location.org)
# 'borrow' the function 'location_to_query' from the Mongo modulestore implementation
items = self.fs_files.find(location_to_query(course_filter))
return list(items)

View File

@@ -211,7 +211,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
template_dir_name = 'course'
def __init__(self, *args, **kwargs):
super(CourseDescriptor, self).__init__(*args, **kwargs)
@@ -233,6 +232,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
# disable the syllabus content for courses that do not provide a syllabus
self.syllabus_present = self.system.resources_fs.exists(path('syllabus'))
self._grading_policy = {}
self.set_grading_policy(self.grading_policy)
self.test_center_exams = []
@@ -421,7 +421,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
policy['GRADE_CUTOFFS'] = value
self.grading_policy = policy
@property
def lowest_passing_grade(self):
return min(self._grading_policy['GRADE_CUTOFFS'].values())
@@ -460,7 +459,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
else:
return self.cohort_config.get("auto_cohort_groups", [])
@property
def top_level_discussion_topic_ids(self):
"""
@@ -469,7 +467,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
topics = self.discussion_topics
return [d["id"] for d in topics.values()]
@property
def cohorted_discussions(self):
"""
@@ -483,8 +480,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
return set(config.get("cohorted_discussions", []))
@property
def is_newish(self):
"""
@@ -585,7 +580,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
yield module_descriptor
for c in self.get_children():
sections = []
for s in c.get_children():
if s.lms.graded:
xmoduledescriptors = list(yield_descriptor_descendents(s))
@@ -601,8 +595,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
all_descriptors.append(s)
return {'graded_sections': graded_sections,
'all_descriptors': all_descriptors, }
'all_descriptors': all_descriptors, }
@staticmethod
def make_id(org, course, url_name):

View File

@@ -122,6 +122,7 @@ div.combined-rubric-container {
span.rubric-category {
font-size: .9em;
font-weight: bold;
}
padding-bottom: 5px;
padding-top: 10px;

View File

@@ -1,8 +1,8 @@
from lxml import etree
from pkg_resources import resource_string, resource_listdir
from pkg_resources import resource_string
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xblock.core import String, Scope
@@ -15,12 +15,11 @@ class DiscussionFields(object):
class DiscussionModule(DiscussionFields, XModule):
js = {'coffee':
[resource_string(__name__, 'js/src/time.coffee'),
resource_string(__name__, 'js/src/discussion/display.coffee')]
}
[resource_string(__name__, 'js/src/time.coffee'),
resource_string(__name__, 'js/src/discussion/display.coffee')]
}
js_module_name = "InlineDiscussion"
def get_html(self):
context = {
'discussion_id': self.discussion_id,
@@ -28,7 +27,7 @@ class DiscussionModule(DiscussionFields, XModule):
return self.system.render_template('discussion/_discussion_module.html', context)
class DiscussionDescriptor(DiscussionFields, RawDescriptor):
class DiscussionDescriptor(DiscussionFields, MetadataOnlyEditingDescriptor, RawDescriptor):
module_class = DiscussionModule
template_dir_name = "discussion"

View File

@@ -41,6 +41,18 @@ class XMLEditingDescriptor(EditingDescriptor):
js_module_name = "XMLEditingDescriptor"
class MetadataOnlyEditingDescriptor(EditingDescriptor):
"""
Module which only provides an editing interface for the metadata, it does
not expose a UI for editing the module data
"""
js = {'coffee': [resource_string(__name__, 'js/src/raw/edit/metadata-only.coffee')]}
js_module_name = "MetadataOnlyEditingDescriptor"
mako_template = "widgets/metadata-only-edit.html"
class JSONEditingDescriptor(EditingDescriptor):
"""
Module that provides a raw editing view of its data as XML. It does not perform

View File

@@ -38,7 +38,7 @@ class ErrorModule(ErrorFields, XModule):
'staff_access': True,
'data': self.contents,
'error': self.error_msg,
})
})
class NonStaffErrorModule(ErrorFields, XModule):
@@ -51,7 +51,7 @@ class NonStaffErrorModule(ErrorFields, XModule):
'staff_access': False,
'data': "",
'error': "",
})
})
class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):

View File

@@ -1,9 +1,11 @@
class InvalidDefinitionError(Exception):
pass
class NotFoundError(Exception):
pass
class ProcessingError(Exception):
'''
An error occurred while processing a request to the XModule.

View File

@@ -51,6 +51,8 @@ class Date(ModelType):
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
class Timedelta(ModelType):
def from_json(self, time_str):
"""

View File

@@ -107,7 +107,7 @@ class FolditModule(FolditFields, XModule):
'show_leader': showleader,
'folditbasic': self.get_basicpuzzles_html(),
'folditchallenge': self.get_challenge_html()
}
}
return self.system.render_template('foldit.html', context)
@@ -124,7 +124,7 @@ class FolditModule(FolditFields, XModule):
'success': self.is_complete(),
'goal_level': goal_level,
'completed': self.completed_puzzles(),
}
}
return self.system.render_template('folditbasic.html', context)
def get_challenge_html(self):
@@ -149,7 +149,6 @@ class FolditModule(FolditFields, XModule):
return 1
class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
"""
Module for adding Foldit problems to courses

View File

@@ -118,8 +118,8 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
with system.resources_fs.open(filepath) as file:
html = file.read().decode('utf-8')
# Log a warning if we can't parse the file, but don't error
if not check_html(html):
msg = "Couldn't parse html in {0}.".format(filepath)
if not check_html(html) and len(html) > 0:
msg = "Couldn't parse html in {0}, content = {1}".format(filepath, html)
log.warning(msg)
system.error_tracker("Warning: " + msg)
@@ -156,7 +156,8 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True)
with resource_fs.open(filepath, 'w') as file:
file.write(self.data.encode('utf-8'))
html_data = self.data.encode('utf-8')
file.write(html_data)
# write out the relative name
relname = path(pathname).basename()

View File

@@ -90,6 +90,7 @@ class @CombinedOpenEnded
@element=element
@reinitialize(element)
$(window).keydown @keydown_handler
$(window).keyup @keyup_handler
reinitialize: (element) ->
@wrapper=$(element).find('section.xmodule_CombinedOpenEndedModule')
@@ -104,6 +105,7 @@ class @CombinedOpenEnded
@location = @el.data('location')
# set up handlers for click tracking
Rubric.initialize(@location)
@is_ctrl = false
@allow_reset = @el.data('allow_reset')
@reset_button = @$('.reset-button')
@@ -322,6 +324,7 @@ class @CombinedOpenEnded
save_answer: (event) =>
event.preventDefault()
max_filesize = 2*1000*1000 #2MB
pre_can_upload_files = @can_upload_files
if @child_state == 'initial'
files = ""
if @can_upload_files == true
@@ -353,6 +356,7 @@ class @CombinedOpenEnded
@find_assessment_elements()
@rebind()
else
@can_upload_files = pre_can_upload_files
@gentle_alert response.error
$.ajaxWithPrefix("#{@ajax_url}/save_answer",settings)
@@ -360,10 +364,17 @@ class @CombinedOpenEnded
else
@errors_area.html(@out_of_sync_message)
keydown_handler: (e) =>
# only do anything when the key pressed is the 'enter' key
if e.which == 13 && @child_state == 'assessing' && Rubric.check_complete()
@save_assessment(e)
keydown_handler: (event) =>
#Previously, responses were submitted when hitting enter. Add in a modifier that ensures that ctrl+enter is needed.
if event.which == 17 && @is_ctrl==false
@is_ctrl=true
else if @is_ctrl==true && event.which == 13 && @child_state == 'assessing' && Rubric.check_complete()
@save_assessment(event)
keyup_handler: (event) =>
#Handle keyup event when ctrl key is released
if event.which == 17 && @is_ctrl==true
@is_ctrl=false
save_assessment: (event) =>
event.preventDefault()
@@ -482,8 +493,10 @@ class @CombinedOpenEnded
if @accept_file_upload == "True"
if window.File and window.FileReader and window.FileList and window.Blob
@can_upload_files = true
@file_upload_area.html('<input type="file" class="file-upload-box">')
@file_upload_area.html('<input type="file" class="file-upload-box"><img class="file-upload-preview" src="#" alt="Uploaded image" />')
@file_upload_area.show()
$('.file-upload-preview').hide()
$('.file-upload-box').change @preview_image
else
@gentle_alert 'File uploads are required for this question, but are not supported in this browser. Try the newest version of google chrome. Alternatively, if you have uploaded the image to the web, you can paste a link to it into the answer box.'
@@ -539,3 +552,28 @@ class @CombinedOpenEnded
log_feedback_selection: (event) ->
target_selection = $(event.target).val()
Logger.log 'oe_feedback_response_selected', {value: target_selection}
remove_attribute: (name) =>
if $('.file-upload-preview').attr(name)
$('.file-upload-preview')[0].removeAttribute(name)
preview_image: () =>
if $('.file-upload-box')[0].files && $('.file-upload-box')[0].files[0]
reader = new FileReader()
reader.onload = (e) =>
max_dim = 150
@remove_attribute('src')
@remove_attribute('height')
@remove_attribute('width')
$('.file-upload-preview').attr('src', e.target.result)
height_px = $('.file-upload-preview')[0].height
width_px = $('.file-upload-preview')[0].width
scale_factor = 0
if height_px>width_px
scale_factor = height_px/max_dim
else
scale_factor = width_px/max_dim
$('.file-upload-preview')[0].width = width_px/scale_factor
$('.file-upload-preview')[0].height = height_px/scale_factor
$('.file-upload-preview').show()
reader.readAsDataURL($('.file-upload-box')[0].files[0])

View File

@@ -161,6 +161,7 @@ class @PeerGradingProblem
constructor: (backend) ->
@prompt_wrapper = $('.prompt-wrapper')
@backend = backend
@is_ctrl = false
# get the location of the problem
@@ -183,6 +184,12 @@ class @PeerGradingProblem
@grading_message.hide()
@question_header = $('.question-header')
@question_header.click @collapse_question
@flag_submission_confirmation = $('.flag-submission-confirmation')
@flag_submission_confirmation_button = $('.flag-submission-confirmation-button')
@flag_submission_removal_button = $('.flag-submission-removal-button')
@flag_submission_confirmation_button.click @close_dialog_box
@flag_submission_removal_button.click @remove_flag
@grading_wrapper =$('.grading-wrapper')
@calibration_feedback_panel = $('.calibration-feedback')
@@ -212,6 +219,7 @@ class @PeerGradingProblem
@answer_unknown_checkbox = $('.answer-unknown-checkbox')
$(window).keydown @keydown_handler
$(window).keyup @keyup_handler
@collapse_question()
@@ -233,9 +241,13 @@ class @PeerGradingProblem
@calibration_interstitial_page.hide()
@is_calibrated_check()
@flag_student_checkbox.click =>
@flag_box_checked()
@calibration_feedback_button.hide()
@calibration_feedback_panel.hide()
@error_container.hide()
@flag_submission_confirmation.hide()
@is_calibrated_check()
@@ -283,6 +295,17 @@ class @PeerGradingProblem
#
##########
remove_flag: () =>
@flag_student_checkbox.removeAttr("checked")
@close_dialog_box()
close_dialog_box: () =>
$( ".flag-submission-confirmation" ).dialog('close')
flag_box_checked: () =>
if @flag_student_checkbox.is(':checked')
$( ".flag-submission-confirmation" ).dialog({ height: 400, width: 400 })
# called after we perform an is_student_calibrated check
calibration_check_callback: (response) =>
if response.success
@@ -338,13 +361,19 @@ class @PeerGradingProblem
@grade = Rubric.get_total_score()
keydown_handler: (event) =>
if event.which == 13 && @submit_button.is(':visible')
#Previously, responses were submitted when hitting enter. Add in a modifier that ensures that ctrl+enter is needed.
if event.which == 17 && @is_ctrl==false
@is_ctrl=true
else if event.which == 13 && @submit_button.is(':visible') && @is_ctrl==true
if @calibration
@submit_calibration_essay()
else
@submit_grade()
keyup_handler: (event) =>
#Handle keyup event when ctrl key is released
if event.which == 17 && @is_ctrl==true
@is_ctrl=false
##########
@@ -443,7 +472,6 @@ class @PeerGradingProblem
calibration_wrapper = $('.calibration-feedback-wrapper')
calibration_wrapper.html("<p>The score you gave was: #{@grade}. The actual score is: #{response.actual_score}</p>")
score = parseInt(@grade)
actual_score = parseInt(response.actual_score)
@@ -452,6 +480,11 @@ class @PeerGradingProblem
else
calibration_wrapper.append("<p>You may want to review the rubric again.</p>")
if response.actual_rubric != undefined
calibration_wrapper.append("<div>Instructor Scored Rubric: #{response.actual_rubric}</div>")
if response.actual_feedback!=undefined
calibration_wrapper.append("<div>Instructor Feedback: #{response.actual_feedback}</div>")
# disable score selection and submission from the grading interface
$("input[name='score-selection']").attr('disabled', true)
@submit_button.hide()

View File

@@ -0,0 +1,5 @@
class @MetadataOnlyEditingDescriptor extends XModule.Descriptor
constructor: (@element) ->
save: ->
data: null

View File

@@ -252,7 +252,6 @@ class Location(_LocationBase):
def __repr__(self):
return "Location%s" % repr(tuple(self))
@property
def course_id(self):
"""Return the ID of the Course that this item belongs to by looking
@@ -414,7 +413,6 @@ class ModuleStore(object):
return courses
class ModuleStoreBase(ModuleStore):
'''
Implement interface functionality that can be shared.
@@ -425,6 +423,7 @@ class ModuleStoreBase(ModuleStore):
'''
self._location_errors = {} # location -> ErrorLog
self.metadata_inheritance_cache = None
self.modulestore_update_signal = None # can be set by runtime to route notifications of datastore changes
def _get_errorlog(self, location):
"""

View File

@@ -6,7 +6,6 @@ Passes settings.MODULESTORE as kwargs to MongoModuleStore
from __future__ import absolute_import
from importlib import import_module
from os import environ
from django.conf import settings
@@ -38,7 +37,7 @@ def modulestore(name='default'):
for key in FUNCTION_KEYS:
if key in options:
options[key] = load_function(options[key])
_MODULESTORES[name] = class_(
**options
)

View File

@@ -3,7 +3,6 @@ from datetime import datetime
from . import ModuleStoreBase, Location, namedtuple_to_son
from .exceptions import ItemNotFoundError
from .inheritance import own_metadata
import logging
DRAFT = 'draft'
@@ -107,7 +106,7 @@ class DraftModuleStore(ModuleStoreBase):
"""
return wrap_draft(super(DraftModuleStore, self).clone_item(source, as_draft(location)))
def update_item(self, location, data):
def update_item(self, location, data, allow_not_found=False):
"""
Set the data in the item specified by the location to
data
@@ -116,9 +115,13 @@ class DraftModuleStore(ModuleStoreBase):
data: A nested dictionary of problem data
"""
draft_loc = as_draft(location)
draft_item = self.get_item(location)
if not getattr(draft_item, 'is_draft', False):
self.clone_item(location, draft_loc)
try:
draft_item = self.get_item(location)
if not getattr(draft_item, 'is_draft', False):
self.clone_item(location, draft_loc)
except ItemNotFoundError, e:
if not allow_not_found:
raise e
return super(DraftModuleStore, self).update_item(draft_loc, data)
@@ -164,7 +167,6 @@ class DraftModuleStore(ModuleStoreBase):
"""
return super(DraftModuleStore, self).delete_item(as_draft(location))
def get_parent_locations(self, location, course_id):
'''Find all locations that are the parents of this location. Needed
for path_to_location().
@@ -178,6 +180,7 @@ class DraftModuleStore(ModuleStoreBase):
Save a current draft to the underlying modulestore
"""
draft = self.get_item(location)
draft.cms.published_date = datetime.utcnow()
draft.cms.published_by = published_by_id
super(DraftModuleStore, self).update_item(location, draft._model_data._kvs._data)
@@ -221,6 +224,6 @@ class DraftModuleStore(ModuleStoreBase):
# convert the dict - which is used for look ups - back into a list
for key, value in to_process_dict.iteritems():
queried_children.append(value)
queried_children.append(value)
return queried_children

View File

@@ -9,9 +9,10 @@ INHERITABLE_METADATA = (
# intended to be set per-course, but can be overridden in for specific
# elements. Can be a float.
'days_early_for_beta',
'giturl' # for git edit link
'giturl' # for git edit link
)
def compute_inherited_metadata(descriptor):
"""Given a descriptor, traverse all of its descendants and do metadata
inheritance. Should be called on a CourseDescriptor after importing a

View File

@@ -7,8 +7,8 @@ from collections import namedtuple
from fs.osfs import OSFS
from itertools import repeat
from path import path
from datetime import datetime
from operator import attrgetter
from uuid import uuid4
from importlib import import_module
from xmodule.errortracker import null_error_tracker, exc_info_to_str
@@ -31,6 +31,12 @@ log = logging.getLogger(__name__)
# that assumption will have to change
def get_course_id_no_run(location):
'''
'''
return "/".join([location.org, location.course])
class MongoKeyValueStore(KeyValueStore):
"""
A KeyValueStore that maps keyed data access to one of the 3 data areas
@@ -125,8 +131,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
render_template: a function for rendering templates, as per
MakoDescriptorSystem
"""
super(CachingDescriptorSystem, self).__init__(
self.load_item, resources_fs, error_tracker, render_template)
super(CachingDescriptorSystem, self).__init__(self.load_item, resources_fs,
error_tracker, render_template)
self.modulestore = modulestore
self.module_data = module_data
self.default_class = default_class
@@ -135,7 +141,6 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
self.course_id = None
self.cached_metadata = cached_metadata
def load_item(self, location):
"""
Return an XModule instance for the specified location
@@ -198,7 +203,9 @@ def location_to_query(location, wildcard=True):
if wildcard:
for key, value in query.items():
if value is None:
# don't allow wildcards on revision, since public is set as None, so
# its ambiguous between None as a real value versus None=wildcard
if value is None and key != '_id.revision':
del query[key]
return query
@@ -216,7 +223,7 @@ class MongoModuleStore(ModuleStoreBase):
def __init__(self, host, db, collection, fs_root, render_template,
port=27017, default_class=None,
error_tracker=null_error_tracker,
user=None, password=None, request_cache=None,
user=None, password=None, request_cache=None,
metadata_inheritance_cache_subsystem=None, **kwargs):
ModuleStoreBase.__init__(self)
@@ -333,7 +340,7 @@ class MongoModuleStore(ModuleStoreBase):
'''
key = metadata_cache_key(location)
tree = {}
if not force_refresh:
# see if we are first in the request cache (if present)
if self.request_cache is not None and key in self.request_cache.data.get('metadata_inheritance', {}):
@@ -348,7 +355,7 @@ class MongoModuleStore(ModuleStoreBase):
if not tree:
# if not in subsystem, or we are on force refresh, then we have to compute
tree = self.compute_metadata_inheritance_tree(location)
# now write out computed tree to caching subsystem (e.g. memcached), if available
if self.metadata_inheritance_cache_subsystem is not None:
self.metadata_inheritance_cache_subsystem.set(key, tree)
@@ -461,7 +468,7 @@ class MongoModuleStore(ModuleStoreBase):
# if we are loading a course object, if we're not prefetching children (depth != 0) then don't
# bother with the metadata inheritance
return [self._load_item(item, data_cache,
apply_cached_metadata=(item['location']['category']!='course' or depth !=0)) for item in items]
apply_cached_metadata=(item['location']['category'] != 'course' or depth != 0)) for item in items]
def get_courses(self):
'''
@@ -541,8 +548,15 @@ class MongoModuleStore(ModuleStoreBase):
Clone a new item that is a copy of the item at the location `source`
and writes it to `location`
"""
item = None
try:
source_item = self.collection.find_one(location_to_query(source))
# allow for some programmatically generated substitutions in metadata, e.g. Discussion_id's should be auto-generated
for key in source_item['metadata'].keys():
if source_item['metadata'][key] == '$$GUID$$':
source_item['metadata'][key] = uuid4().hex
source_item['_id'] = Location(location).dict()
self.collection.insert(
source_item,
@@ -566,12 +580,19 @@ class MongoModuleStore(ModuleStoreBase):
course.tabs = existing_tabs
self.update_metadata(course.location, course._model_data._kvs._metadata)
return item
except pymongo.errors.DuplicateKeyError:
raise DuplicateItemError(location)
# recompute (and update) the metadata inheritance tree which is cached
self.refresh_cached_metadata_inheritance_tree(Location(location))
self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location))
return item
def fire_updated_modulestore_signal(self, course_id, location):
if self.modulestore_update_signal is not None:
self.modulestore_update_signal.send(self, modulestore=self, course_id=course_id,
location=location)
def get_course_for_item(self, location, depth=0):
'''
@@ -643,6 +664,8 @@ class MongoModuleStore(ModuleStoreBase):
self._update_single_item(location, {'definition.children': children})
# recompute (and update) the metadata inheritance tree which is cached
self.refresh_cached_metadata_inheritance_tree(Location(location))
# fire signal that we've written to DB
self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location))
def update_metadata(self, location, metadata):
"""
@@ -669,6 +692,7 @@ class MongoModuleStore(ModuleStoreBase):
self._update_single_item(location, {'metadata': metadata})
# recompute (and update) the metadata inheritance tree which is cached
self.refresh_cached_metadata_inheritance_tree(loc)
self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location))
def delete_item(self, location):
"""
@@ -686,12 +710,12 @@ class MongoModuleStore(ModuleStoreBase):
course.tabs = [tab for tab in existing_tabs if tab.get('url_slug') != location.name]
self.update_metadata(course.location, own_metadata(course))
self.collection.remove({'_id': Location(location).dict()},
# 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)
# 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.
self.collection.remove({'_id': Location(location).dict()}, safe=self.collection.safe)
# recompute (and update) the metadata inheritance tree which is cached
self.refresh_cached_metadata_inheritance_tree(Location(location))
self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location))
def get_parent_locations(self, location, course_id):
'''Find all locations that are the parents of this location in this
@@ -699,7 +723,7 @@ class MongoModuleStore(ModuleStoreBase):
'''
location = Location.ensure_fully_specified(location)
items = self.collection.find({'definition.children': location.url()},
{'_id': True})
{'_id': True})
return [i['_id'] for i in items]
def get_errored_courses(self):

View File

@@ -3,7 +3,7 @@ from itertools import repeat
from xmodule.course_module import CourseDescriptor
from .exceptions import (ItemNotFoundError, NoPathToItem)
from . import ModuleStore, Location
from . import Location
def path_to_location(modulestore, course_id, location):
@@ -106,7 +106,7 @@ def path_to_location(modulestore, course_id, location):
position_list = []
for path_index in range(2, n - 1):
category = path[path_index].category
if category == 'sequential' or category == 'videosequence':
if category == 'sequential' or category == 'videosequence':
section_desc = modulestore.get_instance(course_id, path[path_index])
child_locs = [c.location for c in section_desc.get_children()]
# positions are 1-indexed, and should be strings to be consistent with

View File

@@ -1,4 +1,3 @@
import logging
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore import Location
from xmodule.modulestore.mongo import MongoModuleStore
@@ -33,11 +32,11 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
if original_loc.category != 'course':
module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org,
course=dest_location.course)
course=dest_location.course)
else:
# on the course module we also have to update the module name
module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org,
course=dest_location.course, name=dest_location.name)
course=dest_location.course, name=dest_location.name)
print "Cloning module {0} to {1}....".format(original_loc, module.location)
@@ -49,9 +48,9 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
for child_loc_url in module.children:
child_loc = Location(child_loc_url)
child_loc = child_loc._replace(
tag=dest_location.tag,
org=dest_location.org,
course=dest_location.course
tag=dest_location.tag,
org=dest_location.org,
course=dest_location.course
)
new_children.append(child_loc.url())
@@ -67,7 +66,7 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
thumb_loc = Location(thumb["_id"])
content = contentstore.find(thumb_loc)
content.location = content.location._replace(org=dest_location.org,
course=dest_location.course)
course=dest_location.course)
print "Cloning thumbnail {0} to {1}".format(thumb_loc, content.location)
@@ -80,12 +79,12 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
asset_loc = Location(asset["_id"])
content = contentstore.find(asset_loc)
content.location = content.location._replace(org=dest_location.org,
course=dest_location.course)
course=dest_location.course)
# be sure to update the pointer to the thumbnail
if content.thumbnail_location is not None:
content.thumbnail_location = content.thumbnail_location._replace(org=dest_location.org,
course=dest_location.course)
course=dest_location.course)
print "Cloning asset {0} to {1}".format(asset_loc, content.location)
@@ -94,7 +93,7 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
return True
def delete_course(modulestore, contentstore, source_location, commit = False):
def delete_course(modulestore, contentstore, source_location, commit=False):
# first check to see if the modulestore is Mongo backed
if not isinstance(modulestore, MongoModuleStore):
raise Exception("Expected a MongoModuleStore in the runtime. Aborting....")

View File

@@ -0,0 +1,104 @@
import copy
from uuid import uuid4
from django.test import TestCase
from django.conf import settings
import xmodule.modulestore.django
from xmodule.templates import update_templates
class ModuleStoreTestCase(TestCase):
""" Subclass for any test case that uses the mongodb
module store. This populates a uniquely named modulestore
collection with templates before running the TestCase
and drops it they are finished. """
@staticmethod
def flush_mongo_except_templates():
'''
Delete everything in the module store except templates
'''
modulestore = xmodule.modulestore.django.modulestore()
# This query means: every item in the collection
# that is not a template
query = {"_id.course": {"$ne": "templates"}}
# Remove everything except templates
modulestore.collection.remove(query)
@staticmethod
def load_templates_if_necessary():
'''
Load templates into the modulestore only if they do not already exist.
We need the templates, because they are copied to create
XModules such as sections and problems
'''
modulestore = xmodule.modulestore.django.modulestore()
# Count the number of templates
query = {"_id.course": "templates"}
num_templates = modulestore.collection.find(query).count()
if num_templates < 1:
update_templates()
@classmethod
def setUpClass(cls):
'''
Flush the mongo store and set up templates
'''
# Use a uuid to differentiate
# the mongo collections on jenkins.
cls.orig_modulestore = copy.deepcopy(settings.MODULESTORE)
if 'direct' not in settings.MODULESTORE:
settings.MODULESTORE['direct'] = settings.MODULESTORE['default']
settings.MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
settings.MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
xmodule.modulestore.django._MODULESTORES.clear()
print settings.MODULESTORE
TestCase.setUpClass()
@classmethod
def tearDownClass(cls):
'''
Revert to the old modulestore settings
'''
# Clean up by dropping the collection
modulestore = xmodule.modulestore.django.modulestore()
modulestore.collection.drop()
xmodule.modulestore.django._MODULESTORES.clear()
# Restore the original modulestore settings
settings.MODULESTORE = cls.orig_modulestore
def _pre_setup(self):
'''
Remove everything but the templates before each test
'''
# Flush anything that is not a template
ModuleStoreTestCase.flush_mongo_except_templates()
# Check that we have templates loaded; if not, load them
ModuleStoreTestCase.load_templates_if_necessary()
# Call superclass implementation
super(ModuleStoreTestCase, self)._pre_setup()
def _post_teardown(self):
'''
Flush everything we created except the templates
'''
# Flush anything that is not a template
ModuleStoreTestCase.flush_mongo_except_templates()
# Call superclass implementation
super(ModuleStoreTestCase, self)._post_teardown()

View File

@@ -1,4 +1,4 @@
from factory import Factory
from factory import Factory, lazy_attribute_sequence, lazy_attribute
from time import gmtime
from uuid import uuid4
from xmodule.modulestore import Location
@@ -7,21 +7,12 @@ from xmodule.timeparse import stringify_time
from xmodule.modulestore.inheritance import own_metadata
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):
"""
Factory for XModule courses.
"""
ABSTRACT_FACTORY = True
_creation_function = (XMODULE_COURSE_CREATION,)
@classmethod
def _create(cls, target_class, *args, **kwargs):
@@ -33,7 +24,10 @@ class XModuleCourseFactory(Factory):
location = Location('i4x', org, number,
'course', Location.clean(display_name))
store = modulestore('direct')
try:
store = modulestore('direct')
except KeyError:
store = modulestore()
# Write the data to the mongo datastore
new_course = store.clone_item(template, location)
@@ -52,6 +46,10 @@ class XModuleCourseFactory(Factory):
# Update the data in the mongo datastore
store.update_metadata(new_course.location.url(), own_metadata(new_course))
data = kwargs.get('data')
if data is not None:
store.update_item(new_course.location, data)
return new_course
@@ -74,7 +72,19 @@ class XModuleItemFactory(Factory):
"""
ABSTRACT_FACTORY = True
_creation_function = (XMODULE_ITEM_CREATION,)
display_name = None
@lazy_attribute
def category(attr):
template = Location(attr.template)
return template.category
@lazy_attribute
def location(attr):
parent = Location(attr.parent_location)
dest_name = attr.display_name.replace(" ", "_") if attr.display_name is not None else uuid4().hex
return parent._replace(category=attr.category, name=dest_name)
@classmethod
def _create(cls, target_class, *args, **kwargs):
@@ -110,12 +120,7 @@ class XModuleItemFactory(Factory):
# 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)
new_item = store.clone_item(template, kwargs.get('location'))
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
@@ -145,4 +150,7 @@ class ItemFactory(XModuleItemFactory):
parent_location = 'i4x://MITx/999/course/Robot_Super_Course'
template = 'i4x://edx/templates/chapter/Empty'
display_name = 'Section One'
@lazy_attribute_sequence
def display_name(attr, n):
return "{} {}".format(attr.category.title(), n)

View File

@@ -75,7 +75,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
# tags that really need unique names--they store (or should store) state.
need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter',
'videosequence', 'poll_question', 'timelimit')
'videosequence', 'poll_question', 'timelimit')
attr = xml_data.attrib
tag = xml_data.tag
@@ -169,7 +169,6 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
# Didn't load properly. Fall back on loading as an error
# descriptor. This should never error due to formatting.
msg = "Error loading from xml. " + str(err)[:200]
log.warning(msg)
# Normally, we don't want lots of exception traces in our logs from common
@@ -367,7 +366,7 @@ class XMLModuleStore(ModuleStoreBase):
if org is None:
msg = ("No 'org' attribute set for course in {dir}. "
"Using default 'edx'".format(dir=course_dir))
"Using default 'edx'".format(dir=course_dir))
log.warning(msg)
tracker(msg)
org = 'edx'
@@ -376,10 +375,10 @@ class XMLModuleStore(ModuleStoreBase):
if course is None:
msg = ("No 'course' attribute set for course in {dir}."
" Using default '{default}'".format(
dir=course_dir,
default=course_dir
))
" Using default '{default}'".format(dir=course_dir,
default=course_dir
)
)
log.warning(msg)
tracker(msg)
course = course_dir
@@ -445,7 +444,6 @@ class XMLModuleStore(ModuleStoreBase):
log.debug('========> Done with course import from {0}'.format(course_dir))
return course_descriptor
def load_extra_content(self, system, course_descriptor, category, base_dir, course_dir, url_name):
self._load_extra_content(system, course_descriptor, category, base_dir, course_dir)
@@ -453,7 +451,6 @@ class XMLModuleStore(ModuleStoreBase):
if os.path.isdir(base_dir / url_name):
self._load_extra_content(system, course_descriptor, category, base_dir / url_name, course_dir)
def _load_extra_content(self, system, course_descriptor, category, path, course_dir):
for filepath in glob.glob(path / '*'):
@@ -480,7 +477,6 @@ class XMLModuleStore(ModuleStoreBase):
logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e)))
system.error_tracker("ERROR: " + str(e))
def get_instance(self, course_id, location, depth=0):
"""
Returns an XModuleDescriptor instance for the item at
@@ -542,7 +538,6 @@ class XMLModuleStore(ModuleStoreBase):
return items
def get_courses(self, depth=0):
"""
Returns a list of course descriptors. If there were errors on loading,
@@ -567,7 +562,6 @@ class XMLModuleStore(ModuleStoreBase):
"""
raise NotImplementedError("XMLModuleStores are read-only")
def update_children(self, location, children):
"""
Set the children for the item specified by the location to
@@ -578,7 +572,6 @@ class XMLModuleStore(ModuleStoreBase):
"""
raise NotImplementedError("XMLModuleStores are read-only")
def update_metadata(self, location, metadata):
"""
Set the metadata for the item specified by the location to

View File

@@ -1,12 +1,11 @@
import logging
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata
from fs.osfs import OSFS
from json import dumps
def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir):
def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir, draft_modulestore=None):
course = modulestore.get_item(course_location)
@@ -40,6 +39,26 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
policy = {'course/' + course.location.name: own_metadata(course)}
course_policy.write(dumps(policy))
# export draft content
# NOTE: this code assumes that verticals are the top most draftable container
# should we change the application, then this assumption will no longer
# be valid
if draft_modulestore is not None:
draft_verticals = draft_modulestore.get_items([None, course_location.org, course_location.course,
'vertical', None, 'draft'])
if len(draft_verticals) > 0:
draft_course_dir = export_fs.makeopendir('drafts')
for draft_vertical in draft_verticals:
parent_locs = draft_modulestore.get_parent_locations(draft_vertical.location, course.location.course_id)
# Don't try to export orphaned items.
if len(parent_locs) > 0:
logging.debug('parent_locs = {0}'.format(parent_locs))
draft_vertical.xml_attributes['parent_sequential_url'] = Location(parent_locs[0]).url()
sequential = modulestore.get_item(Location(parent_locs[0]))
index = sequential.children.index(draft_vertical.location.url())
draft_vertical.xml_attributes['index_in_children_list'] = str(index)
draft_vertical.export_to_xml(draft_course_dir)
def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix=''):
query_loc = Location('i4x', course_location.org, course_location.course, category_type, None)

View File

@@ -6,17 +6,17 @@ from path import path
from xblock.core import Scope
from .xml import XMLModuleStore
from .exceptions import DuplicateItemError
from .xml import XMLModuleStore, ImportSystem, ParentTracker
from xmodule.modulestore import Location
from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX
from xmodule.contentstore.content import StaticContent
from .inheritance import own_metadata
from xmodule.errortracker import make_error_tracker
log = logging.getLogger(__name__)
def import_static_content(modules, course_loc, course_data_path, static_content_store, target_location_namespace,
subpath='static', verbose=False):
subpath='static', verbose=False):
remap_dict = {}
@@ -107,10 +107,10 @@ def import_module_from_xml(modulestore, static_content_store, course_data_path,
# the caller passed in
if module.location.category != 'course':
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
course=target_location_namespace.course)
course=target_location_namespace.course)
else:
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
course=target_location_namespace.course, name=target_location_namespace.name)
course=target_location_namespace.course, name=target_location_namespace.name)
# then remap children pointers since they too will be re-namespaced
if module.has_children:
@@ -119,7 +119,7 @@ def import_module_from_xml(modulestore, static_content_store, course_data_path,
for child in children_locs:
child_loc = Location(child)
new_child_loc = child_loc._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
course=target_location_namespace.course)
course=target_location_namespace.course)
new_locs.append(new_child_loc.url())
@@ -139,8 +139,7 @@ def import_module_from_xml(modulestore, static_content_store, course_data_path,
# 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))
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])
@@ -163,9 +162,9 @@ def import_course_from_xml(modulestore, static_content_store, course_data_path,
# 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
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
# 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
@@ -175,7 +174,8 @@ def import_course_from_xml(modulestore, static_content_store, course_data_path,
def import_from_xml(store, data_dir, course_dirs=None,
default_class='xmodule.raw_module.RawDescriptor',
load_error_modules=True, static_content_store=None, target_location_namespace=None, verbose=False):
load_error_modules=True, static_content_store=None, target_location_namespace=None,
verbose=False, draft_store=None):
"""
Import the specified xml data_dir into the "store" modulestore,
using org and course as the location org and course.
@@ -190,7 +190,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
"""
module_store = XMLModuleStore(
xml_module_store = XMLModuleStore(
data_dir,
default_class=default_class,
course_dirs=course_dirs,
@@ -201,7 +201,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
# to enumerate the entire collection of course modules. It will be left as a TBD to implement that
# method on XmlModuleStore.
course_items = []
for course_id in module_store.modules.keys():
for course_id in xml_module_store.modules.keys():
if target_location_namespace is not None:
pseudo_course_id = '/'.join([target_location_namespace.org, target_location_namespace.course])
@@ -222,7 +222,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
# 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():
for module in xml_module_store.modules[course_id].itervalues():
if module.category == 'course':
course_data_path = path(data_dir) / module.data_dir
course_location = module.location
@@ -235,15 +235,11 @@ def import_from_xml(store, data_dir, course_dirs=None,
# 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
{"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)))
import_module(module, store, course_data_path, static_content_store)
# 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
@@ -251,17 +247,16 @@ def import_from_xml(store, data_dir, course_dirs=None,
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)
import_static_content(xml_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():
for module in xml_module_store.modules[course_id].itervalues():
if module.category == 'course':
# we've already saved the course module up at the top of the loop
@@ -275,59 +270,149 @@ def import_from_xml(store, data_dir, course_dirs=None,
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
import_module(module, store, course_data_path, static_content_store)
if 'data' in content:
module_data = content['data']
# now import any 'draft' items
if draft_store is not None:
import_course_draft(xml_module_store, draft_store, course_data_path,
static_content_store, target_location_namespace if target_location_namespace is not None
else course_location)
# 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 <img src='foo.jpg' /> -> <img src='bar.jpg'>
# 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:
logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
else:
module_data = content
store.update_item(module.location, module_data)
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:
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)
store.refresh_cached_metadata_inheritance_tree(target_location_namespace if
target_location_namespace is not None else course_location)
return xml_module_store, course_items
def import_module(module, store, course_data_path, static_content_store, allow_not_found=False):
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
module_data = {}
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 <img src='foo.jpg' /> -> <img src='bar.jpg'>
# 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:
logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
else:
module_data = content
if allow_not_found:
store.update_item(module.location, module_data, allow_not_found=allow_not_found)
else:
store.update_item(module.location, module_data)
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)))
def import_course_draft(xml_module_store, store, course_data_path, static_content_store, target_location_namespace):
'''
This will import all the content inside of the 'drafts' folder, if it exists
NOTE: This is not a full course import, basically in our current application only verticals (and downwards)
can be in draft. Therefore, we need to use slightly different call points into the import process_xml
as we can't simply call XMLModuleStore() constructor (like we do for importing public content)
'''
draft_dir = course_data_path + "/drafts"
if not os.path.exists(draft_dir):
return
# create a new 'System' object which will manage the importing
errorlog = make_error_tracker()
system = ImportSystem(
xml_module_store,
target_location_namespace.course_id,
draft_dir,
{},
errorlog.tracker,
ParentTracker(),
None,
)
# now walk the /vertical directory where each file in there will be a draft copy of the Vertical
for dirname, dirnames, filenames in os.walk(draft_dir + "/vertical"):
for filename in filenames:
module_path = os.path.join(dirname, filename)
with open(module_path) as f:
try:
xml = f.read().decode('utf-8')
descriptor = system.process_xml(xml)
def _import_module(module):
module.location = module.location._replace(revision='draft')
# make sure our parent has us in its list of children
# this is to make sure private only verticals show up in the list of children since
# they would have been filtered out from the non-draft store export
if module.location.category == 'vertical':
module.location = module.location._replace(revision=None)
sequential_url = module.xml_attributes['parent_sequential_url']
index = int(module.xml_attributes['index_in_children_list'])
seq_location = Location(sequential_url)
# IMPORTANT: Be sure to update the sequential in the NEW namespace
seq_location = seq_location._replace(org=target_location_namespace.org,
course=target_location_namespace.course
)
sequential = store.get_item(seq_location)
if module.location.url() not in sequential.children:
sequential.children.insert(index, module.location.url())
store.update_children(sequential.location, sequential.children)
del module.xml_attributes['parent_sequential_url']
del module.xml_attributes['index_in_children_list']
import_module(module, store, course_data_path, static_content_store, allow_not_found=True)
for child in module.get_children():
_import_module(child)
# HACK: since we are doing partial imports of drafts
# the vertical doesn't have the 'url-name' set in the attributes (they are normally in the parent
# object, aka sequential), so we have to replace the location.name with the XML filename
# that is part of the pack
fn, fileExtension = os.path.splitext(filename)
descriptor.location = descriptor.location._replace(name=fn)
_import_module(descriptor)
except Exception, e:
logging.exception('There was an error. {0}'.format(unicode(e)))
pass
return module_store, course_items
def remap_namespace(module, target_location_namespace):
if target_location_namespace is None:
@@ -337,20 +422,20 @@ def remap_namespace(module, target_location_namespace):
# the caller passed in
if module.location.category != 'course':
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
course=target_location_namespace.course)
course=target_location_namespace.course)
else:
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
course=target_location_namespace.course, name=target_location_namespace.name)
course=target_location_namespace.course, name=target_location_namespace.name)
# then remap children pointers since they too will be re-namespaced
if hasattr(module,'children'):
if hasattr(module, 'children'):
children_locs = module.children
if children_locs is not None and children_locs != []:
new_locs = []
for child in children_locs:
child_loc = Location(child)
new_child_loc = child_loc._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
course=target_location_namespace.course)
course=target_location_namespace.course)
new_locs.append(new_child_loc.url())
@@ -365,7 +450,7 @@ def allowed_metadata_by_category(category):
'vertical': [],
'chapter': ['start'],
'sequential': ['due', 'format', 'start', 'graded']
}.get(category,['*'])
}.get(category, ['*'])
def check_module_metadata_editability(module):
@@ -380,7 +465,6 @@ def check_module_metadata_editability(module):
allowed = allowed + ['xml_attributes', 'display_name']
err_cnt = 0
my_metadata = dict(own_metadata(module))
illegal_keys = set(own_metadata(module).keys()) - set(allowed)
if len(illegal_keys) > 0:
@@ -423,7 +507,7 @@ def validate_data_source_path_existence(path, is_err=True, extra_msg=None):
_cnt = 0
if not os.path.exists(path):
print ("{0}: Expected folder at {1}. {2}".format('ERROR' if is_err == True else 'WARNING', path, extra_msg if
extra_msg is not None else ''))
extra_msg is not None else ''))
_cnt = 1
return _cnt
@@ -435,13 +519,13 @@ def validate_data_source_paths(data_dir, course_dir):
warn_cnt = 0
err_cnt += validate_data_source_path_existence(course_path / 'static')
warn_cnt += validate_data_source_path_existence(course_path / 'static/subs', is_err=False,
extra_msg='Video captions (if they are used) will not work unless they are static/subs.')
extra_msg='Video captions (if they are used) will not work unless they are static/subs.')
return err_cnt, warn_cnt
def perform_xlint(data_dir, course_dirs,
default_class='xmodule.raw_module.RawDescriptor',
load_error_modules=True):
default_class='xmodule.raw_module.RawDescriptor',
load_error_modules=True):
err_cnt = 0
warn_cnt = 0
@@ -497,7 +581,6 @@ def perform_xlint(data_dir, course_dirs,
print "WARN: Missing course marketing video. It is recommended that every course have a marketing video."
warn_cnt += 1
print "\n\n------------------------------------------\nVALIDATION SUMMARY: {0} Errors {1} Warnings\n".format(err_cnt, warn_cnt)
if err_cnt > 0:

View File

@@ -131,6 +131,7 @@ class CombinedOpenEndedV1Module():
self.state = instance_state.get('state', self.INITIAL)
self.student_attempts = instance_state.get('student_attempts', 0)
self.weight = instance_state.get('weight', 1)
#Allow reset is true if student has failed the criteria to move to the next child task
self.ready_to_reset = instance_state.get('ready_to_reset', False)
@@ -144,7 +145,7 @@ class CombinedOpenEndedV1Module():
grace_period_string = self.instance_state.get('graceperiod', None)
try:
self.timeinfo = TimeInfo(due_date, grace_period_string)
except:
except Exception:
log.error("Error parsing due date information in location {0}".format(location))
raise
self.display_due_date = self.timeinfo.display_due_date
@@ -362,7 +363,7 @@ class CombinedOpenEndedV1Module():
# if link.startswith(XASSET_SRCREF_PREFIX):
# Placing try except so that if the error is fixed, this code will start working again.
return_html = rewrite_links(html, self.rewrite_content_links)
except:
except Exception:
pass
return return_html
@@ -402,6 +403,7 @@ class CombinedOpenEndedV1Module():
self.static_data, instance_state=task_state)
last_response = task.latest_answer()
last_score = task.latest_score()
all_scores = task.all_scores()
last_post_assessment = task.latest_post_assessment(self.system)
last_post_feedback = ""
feedback_dicts = [{}]
@@ -417,13 +419,18 @@ class CombinedOpenEndedV1Module():
else:
last_post_evaluation = task.format_feedback_with_evaluation(self.system, last_post_assessment)
last_post_assessment = last_post_evaluation
rubric_data = task._parse_score_msg(task.child_history[-1].get('post_assessment', ""), self.system)
rubric_scores = rubric_data['rubric_scores']
grader_types = rubric_data['grader_types']
feedback_items = rubric_data['feedback_items']
feedback_dicts = rubric_data['feedback_dicts']
grader_ids = rubric_data['grader_ids']
submission_ids = rubric_data['submission_ids']
try:
rubric_data = task._parse_score_msg(task.child_history[-1].get('post_assessment', ""), self.system)
except Exception:
log.debug("Could not parse rubric data from child history. "
"Likely we have not yet initialized a previous step, so this is perfectly fine.")
rubric_data = {}
rubric_scores = rubric_data.get('rubric_scores')
grader_types = rubric_data.get('grader_types')
feedback_items = rubric_data.get('feedback_items')
feedback_dicts = rubric_data.get('feedback_dicts')
grader_ids = rubric_data.get('grader_ids')
submission_ids = rubric_data.get('submission_ids')
elif task_type == "selfassessment":
rubric_scores = last_post_assessment
grader_types = ['SA']
@@ -441,7 +448,7 @@ class CombinedOpenEndedV1Module():
human_state = task.HUMAN_NAMES[state]
else:
human_state = state
if len(grader_types) > 0:
if grader_types is not None and len(grader_types) > 0:
grader_type = grader_types[0]
else:
grader_type = "IN"
@@ -454,6 +461,7 @@ class CombinedOpenEndedV1Module():
last_response_dict = {
'response': last_response,
'score': last_score,
'all_scores': all_scores,
'post_assessment': last_post_assessment,
'type': task_type,
'max_score': max_score,
@@ -732,10 +740,37 @@ class CombinedOpenEndedV1Module():
"""
max_score = None
score = None
if self.check_if_done_and_scored():
last_response = self.get_last_response(self.current_task_number)
max_score = last_response['max_score']
score = last_response['score']
if self.is_scored and self.weight is not None:
#Finds the maximum score of all student attempts and keeps it.
score_mat = []
for i in xrange(0, len(self.task_states)):
#For each task, extract all student scores on that task (each attempt for each task)
last_response = self.get_last_response(i)
max_score = last_response.get('max_score', None)
score = last_response.get('all_scores', None)
if score is not None:
#Convert none scores and weight scores properly
for z in xrange(0, len(score)):
if score[z] is None:
score[z] = 0
score[z] *= float(self.weight)
score_mat.append(score)
if len(score_mat) > 0:
#Currently, assume that the final step is the correct one, and that those are the final scores.
#This will change in the future, which is why the machinery above exists to extract all scores on all steps
#TODO: better final score handling.
scores = score_mat[-1]
score = max(scores)
else:
score = 0
if max_score is not None:
#Weight the max score if it is not None
max_score *= float(self.weight)
else:
#Without a max_score, we cannot have a score!
score = None
score_dict = {
'score': score,

View File

@@ -72,7 +72,8 @@ class OpenEndedChild(object):
try:
instance_state = json.loads(instance_state)
except:
log.error("Could not load instance state for open ended. Setting it to nothing.: {0}".format(instance_state))
log.error(
"Could not load instance state for open ended. Setting it to nothing.: {0}".format(instance_state))
else:
instance_state = {}
@@ -81,8 +82,8 @@ class OpenEndedChild(object):
# element.
# Scores are on scale from 0 to max_score
self.child_history=instance_state.get('child_history',[])
self.child_state=instance_state.get('child_state', self.INITIAL)
self.child_history = instance_state.get('child_history', [])
self.child_state = instance_state.get('child_state', self.INITIAL)
self.child_created = instance_state.get('child_created', False)
self.child_attempts = instance_state.get('child_attempts', 0)
@@ -162,6 +163,12 @@ class OpenEndedChild(object):
return None
return self.child_history[-1].get('score')
def all_scores(self):
"""None if not available"""
if not self.child_history:
return None
return [self.child_history[i].get('score') for i in xrange(0, len(self.child_history))]
def latest_post_assessment(self, system):
"""Empty string if not available"""
if not self.child_history:

View File

@@ -291,7 +291,7 @@ class SelfAssessmentDescriptor():
template_dir_name = "selfassessment"
def __init__(self, system):
self.system =system
self.system = system
@classmethod
def definition_from_xml(cls, xml_object, system):

View File

@@ -15,6 +15,7 @@ from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
from xmodule.fields import Date
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
from open_ended_grading_classes import combined_open_ended_rubric
log = logging.getLogger(__name__)
@@ -178,8 +179,14 @@ class PeerGradingModule(PeerGradingFields, XModule):
pass
def get_score(self):
max_score = None
score = None
score_dict = {
'score': score,
'total': max_score,
}
if self.use_for_single_location not in TRUE_DICT or self.is_graded not in TRUE_DICT:
return None
return score_dict
try:
count_graded = self.student_data_for_location['count_graded']
@@ -198,10 +205,11 @@ class PeerGradingModule(PeerGradingFields, XModule):
#Ensures that once a student receives a final score for peer grading, that it does not change.
self.student_data_for_location = response
score_dict = {
'score': int(count_graded >= count_required and count_graded>0) * int(self.weight),
'total': self.max_grade * int(self.weight),
}
if self.weight is not None:
score = int(count_graded >= count_required and count_graded > 0) * float(self.weight)
total = self.max_grade * float(self.weight)
score_dict['score'] = score
score_dict['total'] = total
return score_dict
@@ -384,8 +392,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
# if we can't parse the rubric into HTML,
except etree.XMLSyntaxError:
#This is a dev_facing_error
log.exception("Cannot parse rubric string. Raw string: {0}"
.format(rubric))
log.exception("Cannot parse rubric string.")
#This is a student_facing_error
return {'success': False,
'error': 'Error displaying submission. Please notify course staff.'}
@@ -425,12 +432,15 @@ class PeerGradingModule(PeerGradingFields, XModule):
try:
response = self.peer_gs.save_calibration_essay(location, grader_id, calibration_essay_id,
submission_key, score, feedback, rubric_scores)
if 'actual_rubric' in response:
rubric_renderer = combined_open_ended_rubric.CombinedOpenEndedRubric(self.system, True)
response['actual_rubric'] = rubric_renderer.render_rubric(response['actual_rubric'])['html']
return response
except GradingServiceError:
#This is a dev_facing_error
log.exception(
"Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format(
location, submission_id, submission_key, grader_id))
"Error saving calibration grade, location: {0}, submission_key: {1}, grader_id: {2}".format(
location, submission_key, grader_id))
#This is a student_facing_error
return self._err_response('There was an error saving your score. Please notify course staff.')
@@ -577,5 +587,5 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
stores_state = True
has_score = True
always_recalculate_grades=True
always_recalculate_grades = True
template_dir_name = "peer_grading"

View File

@@ -29,6 +29,6 @@ class RawDescriptor(XmlDescriptor, XMLEditingDescriptor):
line, offset = err.position
msg = ("Unable to create xml for problem {loc}. "
"Context: '{context}'".format(
context=lines[line - 1][offset - 40:offset + 40],
loc=self.location))
context=lines[line - 1][offset - 40:offset + 40],
loc=self.location))
raise Exception, msg, sys.exc_info()[2]

View File

@@ -1,10 +1,9 @@
---
metadata:
display_name: Open Ended Response
max_attempts: 1
attempts: 1
is_graded: False
version: 1
display_name: Open Ended Response
skip_spelling_checks: False
accept_file_upload: False
weight: ""

View File

@@ -2,8 +2,8 @@
metadata:
display_name: Discussion Tag
for: Topic-Level Student-Visible Label
id: 6002x_group_discussion_by_this
id: $$GUID$$
discussion_category: Week 1
data: |
<discussion for="Topic-Level Student-Visible Label" id="6002x_group_discussion_by_this" discussion_category="Week 1" />
<discussion />
children: []

View File

@@ -1,7 +1,6 @@
---
metadata:
display_name: E-text Written in LaTeX
source_processor_url: https://qisx.mit.edu:5443/latex2edx
display_name: E-text Written in LaTeX
source_code: |
\subsection{Example of E-text in LaTeX}

View File

@@ -1,7 +1,6 @@
---
metadata:
display_name: Peer Grading Interface
attempts: 1
use_for_single_location: False
link_to_location: None
is_graded: False

View File

@@ -1,7 +1,6 @@
---
metadata:
display_name: Problem Written in LaTeX
source_processor_url: https://studio-input-filter.mitx.mit.edu/latex2edx
display_name: Problem Written in LaTeX
source_code: |
% Nearly any kind of edX problem can be authored using Latex as
% the source language. Write latex as usual, including equations. The

View File

@@ -1,7 +1,6 @@
---
metadata:
display_name: Problem with Adaptive Hint
source_processor_url: https://qisx.mit.edu:5443/latex2edx
source_code: |
\subsection{Problem With Adaptive Hint}

View File

@@ -1,17 +1,19 @@
"""Tests of the Capa XModule"""
#pylint: disable=C0111
#pylint: disable=R0904
#pylint: disable=C0103
#pylint: disable=C0302
import datetime
import json
from mock import Mock, MagicMock, patch
from pprint import pprint
from mock import Mock, patch
import unittest
import random
import xmodule
import capa
from capa.responsetypes import StudentInputError, \
LoncapaProblemError, ResponseError
LoncapaProblemError, ResponseError
from xmodule.capa_module import CapaModule
from xmodule.modulestore import Location
from lxml import etree
from django.http import QueryDict
@@ -384,7 +386,7 @@ class CapaModuleTest(unittest.TestCase):
# what the input is, by patching CorrectMap.is_correct()
# Also simulate rendering the HTML
# TODO: pep8 thinks the following line has invalid syntax
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct,\
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct, \
patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
mock_is_correct.return_value = True
mock_html.return_value = "Test HTML"
@@ -435,32 +437,38 @@ class CapaModuleTest(unittest.TestCase):
self.assertEqual(module.attempts, 3)
def test_check_problem_resubmitted_with_randomize(self):
# Randomize turned on
module = CapaFactory.create(rerandomize='always', attempts=0)
rerandomize_values = ['always', 'true']
# Simulate that the problem is completed
module.done = True
for rerandomize in rerandomize_values:
# Randomize turned on
module = CapaFactory.create(rerandomize=rerandomize, attempts=0)
# Expect that we cannot submit
with self.assertRaises(xmodule.exceptions.NotFoundError):
get_request_dict = {CapaFactory.input_key(): '3.14'}
module.check_problem(get_request_dict)
# Simulate that the problem is completed
module.done = True
# Expect that number of attempts NOT incremented
self.assertEqual(module.attempts, 0)
# Expect that we cannot submit
with self.assertRaises(xmodule.exceptions.NotFoundError):
get_request_dict = {CapaFactory.input_key(): '3.14'}
module.check_problem(get_request_dict)
# Expect that number of attempts NOT incremented
self.assertEqual(module.attempts, 0)
def test_check_problem_resubmitted_no_randomize(self):
# Randomize turned off
module = CapaFactory.create(rerandomize='never', attempts=0, done=True)
rerandomize_values = ['never', 'false', 'per_student']
# Expect that we can submit successfully
get_request_dict = {CapaFactory.input_key(): '3.14'}
result = module.check_problem(get_request_dict)
for rerandomize in rerandomize_values:
# Randomize turned off
module = CapaFactory.create(rerandomize=rerandomize, attempts=0, done=True)
self.assertEqual(result['success'], 'correct')
# Expect that we can submit successfully
get_request_dict = {CapaFactory.input_key(): '3.14'}
result = module.check_problem(get_request_dict)
# Expect that number of attempts IS incremented
self.assertEqual(module.attempts, 1)
self.assertEqual(result['success'], 'correct')
# Expect that number of attempts IS incremented
self.assertEqual(module.attempts, 1)
def test_check_problem_queued(self):
module = CapaFactory.create(attempts=1)
@@ -615,24 +623,34 @@ class CapaModuleTest(unittest.TestCase):
self.assertTrue('success' in result and not result['success'])
def test_save_problem_submitted_with_randomize(self):
module = CapaFactory.create(rerandomize='always', done=True)
# Try to save
get_request_dict = {CapaFactory.input_key(): '3.14'}
result = module.save_problem(get_request_dict)
# Capa XModule treats 'always' and 'true' equivalently
rerandomize_values = ['always', 'true']
# Expect that we cannot save
self.assertTrue('success' in result and not result['success'])
for rerandomize in rerandomize_values:
module = CapaFactory.create(rerandomize=rerandomize, done=True)
# Try to save
get_request_dict = {CapaFactory.input_key(): '3.14'}
result = module.save_problem(get_request_dict)
# Expect that we cannot save
self.assertTrue('success' in result and not result['success'])
def test_save_problem_submitted_no_randomize(self):
module = CapaFactory.create(rerandomize='never', done=True)
# Try to save
get_request_dict = {CapaFactory.input_key(): '3.14'}
result = module.save_problem(get_request_dict)
# Capa XModule treats 'false' and 'per_student' equivalently
rerandomize_values = ['never', 'false', 'per_student']
# Expect that we succeed
self.assertTrue('success' in result and result['success'])
for rerandomize in rerandomize_values:
module = CapaFactory.create(rerandomize=rerandomize, done=True)
# Try to save
get_request_dict = {CapaFactory.input_key(): '3.14'}
result = module.save_problem(get_request_dict)
# Expect that we succeed
self.assertTrue('success' in result and result['success'])
def test_check_button_name(self):
@@ -681,21 +699,30 @@ class CapaModuleTest(unittest.TestCase):
# If user submitted a problem but hasn't reset,
# do NOT show the check button
# Note: we can only reset when rerandomize="always"
# Note: we can only reset when rerandomize="always" or "true"
module = CapaFactory.create(rerandomize="always", done=True)
self.assertFalse(module.should_show_check_button())
module = CapaFactory.create(rerandomize="true", done=True)
self.assertFalse(module.should_show_check_button())
# Otherwise, DO show the check button
module = CapaFactory.create()
self.assertTrue(module.should_show_check_button())
# If the user has submitted the problem
# and we do NOT have a reset button, then we can show the check button
# Setting rerandomize to "never" ensures that the reset button
# Setting rerandomize to "never" or "false" ensures that the reset button
# is not shown
module = CapaFactory.create(rerandomize="never", done=True)
self.assertTrue(module.should_show_check_button())
module = CapaFactory.create(rerandomize="false", done=True)
self.assertTrue(module.should_show_check_button())
module = CapaFactory.create(rerandomize="per_student", done=True)
self.assertTrue(module.should_show_check_button())
def test_should_show_reset_button(self):
attempts = random.randint(1, 10)
@@ -712,6 +739,14 @@ class CapaModuleTest(unittest.TestCase):
module = CapaFactory.create(rerandomize="never", done=True)
self.assertFalse(module.should_show_reset_button())
# If we're NOT randomizing, then do NOT show the reset button
module = CapaFactory.create(rerandomize="per_student", done=True)
self.assertFalse(module.should_show_reset_button())
# If we're NOT randomizing, then do NOT show the reset button
module = CapaFactory.create(rerandomize="false", done=True)
self.assertFalse(module.should_show_reset_button())
# If the user hasn't submitted an answer yet,
# then do NOT show the reset button
module = CapaFactory.create(done=False)
@@ -742,13 +777,19 @@ class CapaModuleTest(unittest.TestCase):
module = CapaFactory.create(rerandomize="always", done=True)
self.assertFalse(module.should_show_save_button())
module = CapaFactory.create(rerandomize="true", done=True)
self.assertFalse(module.should_show_save_button())
# If the user has unlimited attempts and we are not randomizing,
# then do NOT show a save button
# because they can keep using "Check"
module = CapaFactory.create(max_attempts=None, rerandomize="never", done=False)
self.assertFalse(module.should_show_save_button())
module = CapaFactory.create(max_attempts=None, rerandomize="never", done=True)
module = CapaFactory.create(max_attempts=None, rerandomize="false", done=True)
self.assertFalse(module.should_show_save_button())
module = CapaFactory.create(max_attempts=None, rerandomize="per_student", done=True)
self.assertFalse(module.should_show_save_button())
# Otherwise, DO show the save button
@@ -759,6 +800,12 @@ class CapaModuleTest(unittest.TestCase):
module = CapaFactory.create(rerandomize="never", max_attempts=2, done=True)
self.assertTrue(module.should_show_save_button())
module = CapaFactory.create(rerandomize="false", max_attempts=2, done=True)
self.assertTrue(module.should_show_save_button())
module = CapaFactory.create(rerandomize="per_student", max_attempts=2, done=True)
self.assertTrue(module.should_show_save_button())
# If survey question for capa (max_attempts = 0),
# DO show the save button
module = CapaFactory.create(max_attempts=0, done=False)
@@ -788,9 +835,15 @@ class CapaModuleTest(unittest.TestCase):
done=True)
self.assertTrue(module.should_show_save_button())
module = CapaFactory.create(force_save_button="true",
rerandomize="true",
done=True)
self.assertTrue(module.should_show_save_button())
def test_no_max_attempts(self):
module = CapaFactory.create(max_attempts='')
html = module.get_problem_html()
self.assertTrue(html is not None)
# assert that we got here without exploding
def test_get_problem_html(self):
@@ -875,6 +928,8 @@ class CapaModuleTest(unittest.TestCase):
# Try to render the module with DEBUG turned off
html = module.get_problem_html()
self.assertTrue(html is not None)
# Check the rendering context
render_args, _ = module.system.render_template.call_args
context = render_args[1]
@@ -886,7 +941,9 @@ class CapaModuleTest(unittest.TestCase):
def test_random_seed_no_change(self):
# Run the test for each possible rerandomize value
for rerandomize in ['never', 'per_student', 'always', 'onreset']:
for rerandomize in ['false', 'never',
'per_student', 'always',
'true', 'onreset']:
module = CapaFactory.create(rerandomize=rerandomize)
# Get the seed
@@ -896,8 +953,9 @@ class CapaModuleTest(unittest.TestCase):
# If we're not rerandomizing, the seed is always set
# to the same value (1)
if rerandomize == 'never':
self.assertEqual(seed, 1)
if rerandomize in ['never']:
self.assertEqual(seed, 1,
msg="Seed should always be 1 when rerandomize='%s'" % rerandomize)
# Check the problem
get_request_dict = {CapaFactory.input_key(): '3.14'}
@@ -947,7 +1005,8 @@ class CapaModuleTest(unittest.TestCase):
return success
# Run the test for each possible rerandomize value
for rerandomize in ['never', 'per_student', 'always', 'onreset']:
for rerandomize in ['never', 'false', 'per_student',
'always', 'true', 'onreset']:
module = CapaFactory.create(rerandomize=rerandomize)
# Get the seed
@@ -959,7 +1018,7 @@ class CapaModuleTest(unittest.TestCase):
# is set to 'never' -- it should still be 1
# The seed also stays the same if we're randomizing
# 'per_student': the same student should see the same problem
if rerandomize in ['never', 'per_student']:
if rerandomize in ['never', 'false', 'per_student']:
self.assertEqual(seed, _reset_and_get_seed(module))
# Otherwise, we expect the seed to change
@@ -969,10 +1028,8 @@ class CapaModuleTest(unittest.TestCase):
# Since there's a small chance we might get the
# same seed again, give it 5 chances
# to generate a different seed
success = _retry_and_check(5,
lambda: _reset_and_get_seed(module) != seed)
success = _retry_and_check(5, lambda: _reset_and_get_seed(module) != seed)
# TODO: change this comparison to module.seed is not None?
self.assertTrue(module.seed != None)
self.assertTrue(module.seed is not None)
msg = 'Could not get a new seed from reset after 5 tries'
self.assertTrue(success, msg)

Some files were not shown because too many files have changed in this diff Show More