Merge branch 'master' into drupal-new
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,6 +8,7 @@
|
||||
:2e#
|
||||
.AppleDouble
|
||||
database.sqlite
|
||||
private-requirements.txt
|
||||
courseware/static/js/mathjax/*
|
||||
flushdb.sh
|
||||
build
|
||||
@@ -31,3 +32,4 @@ cover_html/
|
||||
chromedriver.log
|
||||
/nbproject
|
||||
ghostdriver.log
|
||||
node_modules
|
||||
|
||||
20
.pylintrc
20
.pylintrc
@@ -34,10 +34,13 @@ 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
|
||||
# C0302: Too many lines in module
|
||||
# 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
|
||||
@@ -96,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,content
|
||||
generated-members=
|
||||
REQUEST,
|
||||
acl_users,
|
||||
aq_parent,
|
||||
objects,
|
||||
DoesNotExist,
|
||||
can_read,
|
||||
can_write,
|
||||
get_url,
|
||||
size,
|
||||
content,
|
||||
status_code
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
1
.ruby-gemset
Normal file
1
.ruby-gemset
Normal file
@@ -0,0 +1 @@
|
||||
mitx
|
||||
@@ -22,5 +22,4 @@ libreadline6
|
||||
libreadline6-dev
|
||||
mongodb
|
||||
nodejs
|
||||
npm
|
||||
coffeescript
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"'])
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>...]")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -6,18 +6,17 @@ 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
|
||||
@@ -39,6 +38,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
|
||||
@@ -48,6 +48,7 @@ class MongoCollectionFindWrapper(object):
|
||||
self.counter = self.counter+1
|
||||
return self.original(query, *args, **kwargs)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
|
||||
class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
"""
|
||||
@@ -94,6 +95,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
|
||||
@@ -160,32 +188,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'])
|
||||
@@ -231,33 +264,36 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
def test_delete(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
module_store = modulestore('direct')
|
||||
direct_store = modulestore('direct')
|
||||
|
||||
sequential = module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
sequential = direct_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 = direct_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
|
||||
# make sure the parent points to the child object which is to be 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:
|
||||
module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
direct_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
found = True
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
self.assertFalse(found)
|
||||
|
||||
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None]))
|
||||
chapter = direct_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)
|
||||
|
||||
|
||||
def test_about_overrides(self):
|
||||
'''
|
||||
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
|
||||
@@ -359,8 +395,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
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)
|
||||
draft_store.clone_item(child.location, child.location)
|
||||
|
||||
root_dir = path(mkdtemp_clean())
|
||||
|
||||
@@ -375,7 +415,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
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
|
||||
# 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]))
|
||||
|
||||
@@ -438,6 +478,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
for child in vertical.get_children():
|
||||
self.assertTrue(getattr(child, 'is_draft', False))
|
||||
|
||||
# make sure that we don't have a sequential that is in draft mode
|
||||
sequential = draft_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
|
||||
self.assertFalse(getattr(sequential, '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]))
|
||||
@@ -506,15 +552,7 @@ 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:
|
||||
print 'Exception thrown: {0}'.format(traceback.format_exc())
|
||||
pass
|
||||
|
||||
self.assertTrue(exported)
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
|
||||
|
||||
|
||||
class ContentStoreTest(ModuleStoreTestCase):
|
||||
@@ -594,10 +632,12 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
"""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."""
|
||||
@@ -614,10 +654,12 @@ 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"""
|
||||
@@ -630,10 +672,12 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
}
|
||||
|
||||
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"""
|
||||
@@ -649,8 +693,10 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
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."""
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
97
cms/djangoapps/contentstore/tests/test_i18n.py
Normal file
97
cms/djangoapps/contentstore/tests/test_i18n.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from unittest import skip
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import User
|
||||
from django.test.client import Client
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
class InternationalizationTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests to validate Internationalization.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
These tests need a user in the DB so that the django Test Client
|
||||
can log them in.
|
||||
They inherit from the ModuleStoreTestCase class so that the mongodb collection
|
||||
will be cleared out before each test case execution and deleted
|
||||
afterwards.
|
||||
"""
|
||||
self.uname = 'testuser'
|
||||
self.email = 'test+courses@edx.org'
|
||||
self.password = 'foo'
|
||||
|
||||
# Create the use so we can log them in.
|
||||
self.user = User.objects.create_user(self.uname, self.email, self.password)
|
||||
|
||||
# Note that we do not actually need to do anything
|
||||
# for registration if we directly mark them active.
|
||||
self.user.is_active = True
|
||||
# Staff has access to view all courses
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
|
||||
self.course_data = {
|
||||
'template': 'i4x://edx/templates/course/Empty',
|
||||
'org': 'MITx',
|
||||
'number': '999',
|
||||
'display_name': 'Robot Super Course',
|
||||
}
|
||||
|
||||
def test_course_plain_english(self):
|
||||
"""Test viewing the index page with no courses"""
|
||||
self.client = Client()
|
||||
self.client.login(username=self.uname, password=self.password)
|
||||
|
||||
resp = self.client.get(reverse('index'))
|
||||
self.assertContains(resp,
|
||||
'<h1 class="title-1">My Courses</h1>',
|
||||
status_code=200,
|
||||
html=True)
|
||||
|
||||
def test_course_explicit_english(self):
|
||||
"""Test viewing the index page with no courses"""
|
||||
self.client = Client()
|
||||
self.client.login(username=self.uname, password=self.password)
|
||||
|
||||
resp = self.client.get(reverse('index'),
|
||||
{},
|
||||
HTTP_ACCEPT_LANGUAGE='en'
|
||||
)
|
||||
|
||||
self.assertContains(resp,
|
||||
'<h1 class="title-1">My Courses</h1>',
|
||||
status_code=200,
|
||||
html=True)
|
||||
|
||||
|
||||
# ****
|
||||
# NOTE:
|
||||
# ****
|
||||
#
|
||||
# This test will break when we replace this fake 'test' language
|
||||
# with actual French. This test will need to be updated with
|
||||
# actual French at that time.
|
||||
|
||||
# Test temporarily disable since it depends on creation of dummy strings
|
||||
@skip
|
||||
def test_course_with_accents (self):
|
||||
"""Test viewing the index page with no courses"""
|
||||
self.client = Client()
|
||||
self.client.login(username=self.uname, password=self.password)
|
||||
|
||||
resp = self.client.get(reverse('index'),
|
||||
{},
|
||||
HTTP_ACCEPT_LANGUAGE='fr'
|
||||
)
|
||||
|
||||
TEST_STRING = u'<h1 class="title-1">' \
|
||||
+ u'My \xc7\xf6\xfcrs\xe9s L#' \
|
||||
+ u'</h1>'
|
||||
|
||||
self.assertContains(resp,
|
||||
TEST_STRING,
|
||||
status_code=200,
|
||||
html=True)
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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,7 @@ 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):
|
||||
@@ -87,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,
|
||||
@@ -193,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.
|
||||
@@ -209,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.
|
||||
@@ -221,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -619,26 +615,14 @@ def delete_item(request):
|
||||
delete_children = request.POST.get('delete_children', False)
|
||||
delete_all_versions = request.POST.get('delete_all_versions', False)
|
||||
|
||||
item = modulestore().get_item(item_location)
|
||||
store = modulestore()
|
||||
|
||||
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'
|
||||
item = store.get_item(item_location)
|
||||
|
||||
if delete_children:
|
||||
_xmodule_recurse(item, lambda i: store.delete_item(i.location))
|
||||
_xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions))
|
||||
else:
|
||||
store.delete_item(item.location)
|
||||
|
||||
# cdodge: this is a bit of a hack until I can talk with Cale about the
|
||||
# semantics of delete_item whereby the store is draft aware. Right now calling
|
||||
# delete_item on a vertical tries to delete the draft version leaving the
|
||||
# requested delete to never occur
|
||||
if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions:
|
||||
modulestore('direct').delete_item(item.location)
|
||||
store.delete_item(item.location, delete_all_versions)
|
||||
|
||||
# cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
|
||||
if delete_all_versions:
|
||||
@@ -665,7 +649,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 +784,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 +818,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 +861,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 +894,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 +977,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 +994,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 +1022,7 @@ def edit_tabs(request, org, course, coursename):
|
||||
'active_tab': 'pages',
|
||||
'context_course': course_item,
|
||||
'components': components
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
def not_found(request):
|
||||
@@ -1102,21 +1084,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 +1166,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 +1185,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 +1207,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 +1303,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 +1329,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 +1417,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']
|
||||
@@ -1504,6 +1487,12 @@ def create_new_course(request):
|
||||
|
||||
new_course = modulestore('direct').clone_item(template, dest_location)
|
||||
|
||||
# clone a default 'about' module as well
|
||||
|
||||
about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview'])
|
||||
dest_about_location = dest_location._replace(category='about', name='overview')
|
||||
modulestore('direct').clone_item(about_template_location, dest_about_location)
|
||||
|
||||
if display_name is not None:
|
||||
new_course.display_name = display_name
|
||||
|
||||
@@ -1527,10 +1516,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,8 +1575,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), draft_store=modulestore())
|
||||
[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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -36,3 +36,4 @@ DATABASES = {
|
||||
INSTALLED_APPS += ('lettuce.django',)
|
||||
LETTUCE_APPS = ('contentstore',)
|
||||
LETTUCE_SERVER_PORT = 8001
|
||||
LETTUCE_BROWSER = 'chrome'
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -20,11 +20,8 @@ Longer TODO:
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os.path
|
||||
import os
|
||||
import lms.envs.common
|
||||
from path import path
|
||||
from xmodule.static_content import write_descriptor_styles, write_descriptor_js, write_module_js, write_module_styles
|
||||
|
||||
############################ FEATURE CONFIGURATION #############################
|
||||
|
||||
@@ -35,7 +32,7 @@ MITX_FEATURES = {
|
||||
'AUTH_USE_MIT_CERTIFICATES': False,
|
||||
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
|
||||
'STAFF_EMAIL': '', # email address for staff (eg to request course creation)
|
||||
'STUDIO_NPS_SURVEY': True,
|
||||
'STUDIO_NPS_SURVEY': True,
|
||||
'SEGMENT_IO': True,
|
||||
}
|
||||
ENABLE_JASMINE = False
|
||||
@@ -129,6 +126,9 @@ MIDDLEWARE_CLASSES = (
|
||||
'track.middleware.TrackMiddleware',
|
||||
'mitxmako.middleware.MakoMiddleware',
|
||||
|
||||
# Detects user-requested locale from 'accept-language' header in http request
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
|
||||
'django.middleware.transaction.TransactionMiddleware'
|
||||
)
|
||||
|
||||
@@ -167,15 +167,19 @@ 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
|
||||
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
|
||||
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
|
||||
# Localization strings (e.g. django.po) are under this directory
|
||||
LOCALE_PATHS = (REPO_ROOT + '/conf/locale',) # mitx/conf/locale/
|
||||
|
||||
# Tracking
|
||||
TRACK_MAX_EVENT = 10000
|
||||
|
||||
@@ -186,29 +190,7 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
|
||||
|
||||
STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
|
||||
|
||||
# Load javascript and css from all of the available descriptors, and
|
||||
# prep it for use in pipeline js
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from rooted_paths import rooted_glob, remove_root
|
||||
|
||||
write_descriptor_styles(PROJECT_ROOT / "static/sass/descriptor", [RawDescriptor, ErrorDescriptor])
|
||||
write_module_styles(PROJECT_ROOT / "static/sass/module", [RawDescriptor, ErrorDescriptor])
|
||||
|
||||
descriptor_js = remove_root(
|
||||
PROJECT_ROOT / 'static',
|
||||
write_descriptor_js(
|
||||
PROJECT_ROOT / "static/coffee/descriptor",
|
||||
[RawDescriptor, ErrorDescriptor]
|
||||
)
|
||||
)
|
||||
module_js = remove_root(
|
||||
PROJECT_ROOT / 'static',
|
||||
write_module_js(
|
||||
PROJECT_ROOT / "static/coffee/module",
|
||||
[RawDescriptor, ErrorDescriptor]
|
||||
)
|
||||
)
|
||||
from rooted_paths import rooted_glob
|
||||
|
||||
PIPELINE_CSS = {
|
||||
'base-style': {
|
||||
@@ -216,39 +198,35 @@ PIPELINE_CSS = {
|
||||
'js/vendor/CodeMirror/codemirror.css',
|
||||
'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css',
|
||||
'css/vendor/jquery.qtip.min.css',
|
||||
'sass/base-style.scss'
|
||||
'sass/base-style.css',
|
||||
'xmodule/modules.css',
|
||||
'xmodule/descriptor.css',
|
||||
],
|
||||
'output_filename': 'css/cms-base-style.css',
|
||||
},
|
||||
}
|
||||
|
||||
PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss']
|
||||
|
||||
# test_order: Determines the position of this chunk of javascript on
|
||||
# the jasmine test page
|
||||
PIPELINE_JS = {
|
||||
'main': {
|
||||
'source_filenames': sorted(
|
||||
rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.coffee') +
|
||||
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.coffee')
|
||||
rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.js') +
|
||||
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js')
|
||||
) + ['js/hesitate.js', 'js/base.js'],
|
||||
'output_filename': 'js/cms-application.js',
|
||||
'test_order': 0
|
||||
},
|
||||
'module-js': {
|
||||
'source_filenames': descriptor_js + module_js,
|
||||
'source_filenames': (
|
||||
rooted_glob(COMMON_ROOT / 'static/', 'xmodule/descriptors/js/*.js') +
|
||||
rooted_glob(COMMON_ROOT / 'static/', 'xmodule/modules/js/*.js')
|
||||
),
|
||||
'output_filename': 'js/cms-modules.js',
|
||||
'test_order': 1
|
||||
},
|
||||
'spec': {
|
||||
'source_filenames': sorted(rooted_glob(PROJECT_ROOT / 'static/', 'coffee/spec/**/*.coffee')),
|
||||
'output_filename': 'js/cms-spec.js'
|
||||
}
|
||||
}
|
||||
|
||||
PIPELINE_COMPILERS = [
|
||||
'pipeline.compilers.sass.SASSCompiler',
|
||||
'pipeline.compilers.coffee.CoffeeScriptCompiler',
|
||||
]
|
||||
|
||||
PIPELINE_SASS_ARGUMENTS = '-t compressed -r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
|
||||
|
||||
PIPELINE_CSS_COMPRESSOR = None
|
||||
PIPELINE_JS_COMPRESSOR = None
|
||||
|
||||
@@ -260,11 +238,6 @@ STATICFILES_IGNORE_PATTERNS = (
|
||||
)
|
||||
|
||||
PIPELINE_YUI_BINARY = 'yui-compressor'
|
||||
PIPELINE_SASS_BINARY = 'sass'
|
||||
PIPELINE_COFFEE_SCRIPT_BINARY = 'coffee'
|
||||
|
||||
# Setting that will only affect the MITx version of django-pipeline until our changes are merged upstream
|
||||
PIPELINE_COMPILE_INPLACE = True
|
||||
|
||||
############################ APPS #####################################
|
||||
|
||||
|
||||
@@ -20,14 +20,14 @@ PIPELINE_JS['js-test-source'] = {
|
||||
'source_filenames': sum([
|
||||
pipeline_group['source_filenames']
|
||||
for group_name, pipeline_group
|
||||
in PIPELINE_JS.items()
|
||||
in sorted(PIPELINE_JS.items(), key=lambda item: item[1].get('test_order', 1e100))
|
||||
if group_name != 'spec'
|
||||
], []),
|
||||
'output_filename': 'js/cms-test-source.js'
|
||||
}
|
||||
|
||||
PIPELINE_JS['spec'] = {
|
||||
'source_filenames': sorted(rooted_glob(PROJECT_ROOT / 'static/', 'coffee/spec/**/*.coffee')),
|
||||
'source_filenames': sorted(rooted_glob(PROJECT_ROOT / 'static/', 'coffee/spec/**/*.js')),
|
||||
'output_filename': 'js/cms-spec.js'
|
||||
}
|
||||
|
||||
@@ -35,4 +35,10 @@ JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
|
||||
|
||||
STATICFILES_DIRS.append(COMMON_ROOT / 'test' / 'phantom-jasmine' / 'lib')
|
||||
|
||||
# Remove the localization middleware class because it requires the test database
|
||||
# to be sync'd and migrated in order to run the jasmine tests interactively
|
||||
# with a browser
|
||||
MIDDLEWARE_CLASSES = tuple(e for e in MIDDLEWARE_CLASSES \
|
||||
if e != 'django.middleware.locale.LocaleMiddleware')
|
||||
|
||||
INSTALLED_APPS += ('django_jasmine', )
|
||||
|
||||
@@ -13,14 +13,10 @@ 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')
|
||||
|
||||
# Makes the tests run much faster...
|
||||
SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
|
||||
|
||||
# Want static files in the same dir for running on jenkins.
|
||||
STATIC_ROOT = TEST_ROOT / "staticfiles"
|
||||
|
||||
@@ -28,7 +24,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 +37,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 +49,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 +72,7 @@ CONTENTSTORE = {
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ENV_ROOT / "db" / "cms.db",
|
||||
'NAME': TEST_ROOT / "db" / "cms.db",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -121,3 +117,7 @@ PASSWORD_HASHERS = (
|
||||
|
||||
# dummy segment-io key
|
||||
SEGMENT_IO_KEY = '***REMOVED***'
|
||||
|
||||
# disable NPS survey in test mode
|
||||
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
|
||||
|
||||
|
||||
2
cms/static/coffee/.gitignore
vendored
2
cms/static/coffee/.gitignore
vendored
@@ -1,3 +1 @@
|
||||
*.js
|
||||
descriptor
|
||||
module
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"js/vendor/jquery.cookie.js",
|
||||
"js/vendor/json2.js",
|
||||
"js/vendor/underscore-min.js",
|
||||
"js/vendor/backbone-min.js"
|
||||
"js/vendor/backbone-min.js",
|
||||
"js/vendor/jquery.leanModal.min.js"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ function showImportSubmit(e) {
|
||||
$('.submit-button').show();
|
||||
$('.progress').show();
|
||||
} else {
|
||||
$('.error-block').html('File format not supported. Please upload a file with a <code>tar.gz</code> extension.').show();
|
||||
$('.error-block').html(gettext('File format not supported. Please upload a file with a <code>tar.gz</code> extension.')).show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,7 +406,7 @@ function showFileSelectionMenu(e) {
|
||||
}
|
||||
|
||||
function startUpload(e) {
|
||||
$('.upload-modal h1').html('Uploading…');
|
||||
$('.upload-modal h1').html(gettext('Uploading…'));
|
||||
$('.upload-modal .file-name').html($('.file-input').val().replace('C:\\fakepath\\', ''));
|
||||
$('.upload-modal .file-chooser').ajaxSubmit({
|
||||
beforeSend: resetUploadBar,
|
||||
@@ -439,7 +439,7 @@ function displayFinishedUpload(xhr) {
|
||||
$('.upload-modal .embeddable').show();
|
||||
$('.upload-modal .file-name').hide();
|
||||
$('.upload-modal .progress-fill').html(resp.msg);
|
||||
$('.upload-modal .choose-file-button').html('Load Another File').show();
|
||||
$('.upload-modal .choose-file-button').html(gettext('Load Another File')).show();
|
||||
$('.upload-modal .progress-fill').width('100%');
|
||||
|
||||
// see if this id already exists, if so, then user must have updated an existing piece of content
|
||||
@@ -500,11 +500,11 @@ function toggleSock(e) {
|
||||
});
|
||||
|
||||
if($sock.hasClass('is-shown')) {
|
||||
$btnLabel.text('Hide Studio Help');
|
||||
$btnLabel.text(gettext('Hide Studio Help'));
|
||||
}
|
||||
|
||||
else {
|
||||
$btnLabel.text('Looking for Help with Studio?');
|
||||
$btnLabel.text(gettext('Looking for Help with Studio?'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -845,7 +845,15 @@ function saveSetSectionScheduleDate(e) {
|
||||
data: JSON.stringify({ 'id': id, 'metadata': {'start': start}})
|
||||
}).success(function () {
|
||||
var $thisSection = $('.courseware-section[data-id="' + id + '"]');
|
||||
$thisSection.find('.section-published-date').html('<span class="published-status"><strong>Will Release:</strong> ' + input_date + ' at ' + input_time + ' UTC</span><a href="#" class="edit-button" data-date="' + input_date + '" data-time="' + input_time + '" data-id="' + id + '">Edit</a>');
|
||||
var format = gettext('<strong>Will Release:</strong> %(date)s at $(time)s UTC');
|
||||
var willReleaseAt = interpolate(format, [input_date, input_time], true);
|
||||
$thisSection.find('.section-published-date').html(
|
||||
'<span class="published-status">' + willReleaseAt + '</span>' +
|
||||
'<a href="#" class="edit-button" ' +
|
||||
'" data-date="' + input_date +
|
||||
'" data-time="' + input_time +
|
||||
'" data-id="' + id + '">' +
|
||||
gettext('Edit') + '</a>');
|
||||
$thisSection.find('.section-published-date').animate({
|
||||
'background-color': 'rgb(182,37,104)'
|
||||
}, 300).animate({
|
||||
|
||||
2
cms/static/sass/.gitignore
vendored
2
cms/static/sass/.gitignore
vendored
@@ -1,3 +1 @@
|
||||
*.css
|
||||
descriptor
|
||||
module
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
@import 'assets/content-types';
|
||||
|
||||
// xblock-related
|
||||
@import 'module/module-styles.scss';
|
||||
@import 'descriptor/module-styles.scss';
|
||||
@import 'xmodule/modules/css/module-styles.scss';
|
||||
@import 'xmodule/descriptors/css/module-styles.scss';
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../../common/static/sass/bourbon/
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
<body class="<%block name='bodyclass'></%block> hide-wip">
|
||||
<%include file="courseware_vendor_js.html"/>
|
||||
<script type="text/javascript" src="/jsi18n/"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<%inherit file="base.html" />
|
||||
|
||||
<%block name="title">My Courses</%block>
|
||||
<%block name="title">${_("My Courses")}</%block>
|
||||
<%block name="bodyclass">is-signedin index dashboard</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
@@ -36,18 +38,18 @@
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions">
|
||||
<div class="title">
|
||||
<h1 class="title-1">My Courses</h1>
|
||||
<h1 class="title-1">${_("My Courses")}</h1>
|
||||
</div>
|
||||
|
||||
% if user.is_active:
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Page Actions</h3>
|
||||
<h3 class="sr">${_("Page Actions")}</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
% if not disable_course_creation:
|
||||
<a href="#" class="button new-button new-course-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">+</i> New Course</a>
|
||||
<a href="#" class="button new-button new-course-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">+</i> ${_("New Course")}</a>
|
||||
% elif settings.MITX_FEATURES.get('STAFF_EMAIL',''):
|
||||
<a href="mailto:${settings.MITX_FEATURES.get('STAFF_EMAIL','')}">Email staff to create course</a>
|
||||
<a href="mailto:${settings.MITX_FEATURES.get('STAFF_EMAIL','')}">${_("Email staff to create course")}</a>
|
||||
% endif
|
||||
</li>
|
||||
</ul>
|
||||
@@ -59,7 +61,9 @@
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<div class="introduction">
|
||||
<p class="copy"><strong>Welcome, ${ user.username }</strong>. Here are all of the courses you are currently authoring in Studio:</p>
|
||||
<p class="copy">
|
||||
<strong>${_("Welcome, %(name)s") % dict(name= user.username)}</strong>.
|
||||
${_("Here are all of the courses you are currently authoring in Studio:")}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -81,11 +85,11 @@
|
||||
% else:
|
||||
<div class='warn-msg'>
|
||||
<p>
|
||||
In order to start authoring courses using edX Studio, please click on the activation link in your email.
|
||||
${_("In order to start authoring courses using edX Studio, please click on the activation link in your email.")}
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
</%block>
|
||||
|
||||
@@ -87,12 +87,12 @@ from contentstore import utils
|
||||
<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="${utils.get_lms_link_for_about_page(course_location)}" />${utils.get_lms_link_for_about_page(course_location)}</a></p>
|
||||
<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:john.doe@gmail.com?Subject=Enroll%20in%20COURSENAME&body=Hi,%20COURSENAME,%20provided%20by%20edX,%20is%20almost%20ready%20to%20begin.%20Please%20enroll%20for%20this%20course%20at%20${utils.get_lms_link_for_about_page(course_location)}." class="action action-primary"><i class="ss-icon icon ss-symbolicons-standard icon icon-inline icon-announcement">✉</i> Send an invitation to your students</a>
|
||||
<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"${context_course.display_name_with_default}",%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">✉</i> Invite your students</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -179,7 +179,7 @@ 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 class="link-courseURL" rel="external" 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> (formatted in HTML)</span>
|
||||
</li>
|
||||
|
||||
<li class="field video" id="field-course-introduction-video">
|
||||
|
||||
@@ -4,20 +4,20 @@
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%!
|
||||
from contentstore import utils
|
||||
from contentstore import utils
|
||||
%>
|
||||
|
||||
<%block name="jsextra">
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.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/models/settings/course_grading_policy.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/settings/settings_grading_view.js')}"></script>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
|
||||
@@ -26,15 +26,15 @@ from contentstore import utils
|
||||
}).blur(function() {
|
||||
$("label").removeClass("is-focused");
|
||||
});
|
||||
|
||||
|
||||
var editor = new CMS.Views.Settings.Grading({
|
||||
el: $('.settings-grading'),
|
||||
model : new CMS.Models.Settings.CourseGradingPolicy(${course_details|n},{parse:true})
|
||||
});
|
||||
|
||||
|
||||
editor.render();
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -97,7 +97,7 @@ from contentstore import utils
|
||||
<ol class="list-input">
|
||||
<li class="field text" id="field-course-grading-graceperiod">
|
||||
<label for="course-grading-graceperiod">Grace Period on Deadline:</label>
|
||||
<input type="text" class="short time" id="course-grading-graceperiod" value="0:00" placeholder="e.g. 10 minutes">
|
||||
<input type="text" class="short time" id="course-grading-graceperiod" value="0:00" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-inline">Leeway on due dates</span>
|
||||
</li>
|
||||
</ol>
|
||||
@@ -112,13 +112,13 @@ from contentstore import utils
|
||||
</header>
|
||||
|
||||
<ol class="list-input course-grading-assignment-list enum">
|
||||
|
||||
</ol>
|
||||
|
||||
</ol>
|
||||
|
||||
<div class="actions">
|
||||
<a href="#" class="new-button new-course-grading-item add-grading-data">
|
||||
<span class="plus-icon white"></span>New Assignment Type
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<div class="wrapper-footer wrapper">
|
||||
<footer class="primary" role="contentinfo">
|
||||
<div class="colophon">
|
||||
<p>© 2013 <a href="http://www.edx.org" rel="external">edX</a>. All rights reserved.</p>
|
||||
<p>© 2013 <a href="http://www.edx.org" rel="external">edX</a>. ${ _("All rights reserved.")}</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-peripheral">
|
||||
@@ -15,10 +17,11 @@
|
||||
</li> -->
|
||||
% if user.is_authenticated():
|
||||
<li class="nav-item nav-peripheral-feedback">
|
||||
<a href="http://help.edge.edx.org/discussion/new" class="show-tender" title="Use our feedback tool, Tender, to share your feedback">Contact Us</a>
|
||||
<a href="http://help.edge.edx.org/discussion/new" class="show-tender" title="${_('Use our feedback tool, Tender, to share your feedback')}">${_("Contact Us")}</a>
|
||||
</li>
|
||||
% endif
|
||||
</ol>
|
||||
</nav>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
11
cms/urls.py
11
cms/urls.py
@@ -120,6 +120,17 @@ urlpatterns += (
|
||||
url(r'^ux-alerts$', 'contentstore.views.ux_alerts', name='ux-alerts')
|
||||
)
|
||||
|
||||
js_info_dict = {
|
||||
'domain': 'djangojs',
|
||||
'packages': ('cms',),
|
||||
}
|
||||
|
||||
urlpatterns += (
|
||||
# Serve catalog of localized strings to be rendered by Javascript
|
||||
url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict),
|
||||
)
|
||||
|
||||
|
||||
if settings.ENABLE_JASMINE:
|
||||
# # Jasmine
|
||||
urlpatterns = urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
|
||||
|
||||
@@ -4,22 +4,8 @@ Namespace defining common fields used by Studio for all blocks
|
||||
|
||||
import datetime
|
||||
|
||||
from xblock.core import Namespace, Boolean, Scope, ModelType, String
|
||||
|
||||
|
||||
class StringyBoolean(Boolean):
|
||||
"""
|
||||
Reads strings from JSON as booleans.
|
||||
|
||||
If the string is 'true' (case insensitive), then return True,
|
||||
otherwise False.
|
||||
|
||||
JSON values that aren't strings are returned as is
|
||||
"""
|
||||
def from_json(self, value):
|
||||
if isinstance(value, basestring):
|
||||
return value.lower() == 'true'
|
||||
return value
|
||||
from xblock.core import Namespace, Scope, ModelType, String
|
||||
from xmodule.fields import StringyBoolean
|
||||
|
||||
|
||||
class DateTuple(ModelType):
|
||||
|
||||
@@ -12,7 +12,7 @@ from external_auth.djangostore import DjangoOpenIDStore
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import UserProfile
|
||||
from student.models import UserProfile, TestCenterUser, TestCenterRegistration
|
||||
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.utils.http import urlquote
|
||||
@@ -34,6 +34,12 @@ from openid.server.trustroot import TrustRoot
|
||||
from openid.extensions import ax, sreg
|
||||
|
||||
import student.views as student_views
|
||||
# Required for Pearson
|
||||
from courseware.views import get_module_for_descriptor, jump_to
|
||||
from courseware.model_data import ModelDataCache
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
log = logging.getLogger("mitx.external_auth")
|
||||
|
||||
@@ -551,7 +557,7 @@ def provider_login(request):
|
||||
'nickname': user.username,
|
||||
'email': user.email,
|
||||
'fullname': user.username
|
||||
}
|
||||
}
|
||||
|
||||
# the request succeeded:
|
||||
return provider_respond(server, openid_request, response, results)
|
||||
@@ -606,3 +612,140 @@ def provider_xrds(request):
|
||||
# custom XRDS header necessary for discovery process
|
||||
response['X-XRDS-Location'] = get_xrds_url('xrds', request)
|
||||
return response
|
||||
|
||||
|
||||
#-------------------
|
||||
# Pearson
|
||||
#-------------------
|
||||
def course_from_id(course_id):
|
||||
"""Return the CourseDescriptor corresponding to this course_id"""
|
||||
course_loc = CourseDescriptor.id_to_location(course_id)
|
||||
return modulestore().get_instance(course_id, course_loc)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def test_center_login(request):
|
||||
''' Log in students taking exams via Pearson
|
||||
|
||||
Takes a POST request that contains the following keys:
|
||||
- code - a security code provided by Pearson
|
||||
- clientCandidateID
|
||||
- registrationID
|
||||
- exitURL - the url that we redirect to once we're done
|
||||
- vueExamSeriesCode - a code that indicates the exam that we're using
|
||||
'''
|
||||
# errors are returned by navigating to the error_url, adding a query parameter named "code"
|
||||
# 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)
|
||||
|
||||
# get provided error URL, which will be used as a known prefix for returning error messages to the
|
||||
# Pearson shell.
|
||||
error_url = request.POST.get("errorURL")
|
||||
|
||||
# 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"))
|
||||
code = request.POST.get("code")
|
||||
|
||||
# calculate SHA for query string
|
||||
# TODO: figure out how to get the original query string, so we can hash it and compare.
|
||||
|
||||
if 'clientCandidateID' not in request.POST:
|
||||
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
|
||||
# expected values....
|
||||
# registration_id = request.POST.get("registrationID")
|
||||
# exit_url = request.POST.get("exitURL")
|
||||
|
||||
# find testcenter_user that matches the provided ID:
|
||||
try:
|
||||
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"))
|
||||
|
||||
# find testcenter_registration that matches the provided exam code:
|
||||
# Note that we could rely in future on either the registrationId or the exam code,
|
||||
# or possibly both. But for now we know what to do with an ExamSeriesCode,
|
||||
# while we currently have no record of RegistrationID values at all.
|
||||
if 'vueExamSeriesCode' not in request.POST:
|
||||
# we are not allowed to make up a new error code, according to Pearson,
|
||||
# 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"))
|
||||
exam_series_code = request.POST.get('vueExamSeriesCode')
|
||||
|
||||
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"))
|
||||
|
||||
# TODO: figure out what to do if there are more than one registrations....
|
||||
# for now, just take the first...
|
||||
registration = registrations[0]
|
||||
|
||||
course_id = registration.course_id
|
||||
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"))
|
||||
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"))
|
||||
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))
|
||||
|
||||
# check if the test has already been taken
|
||||
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"))
|
||||
|
||||
timelimit_module_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user,
|
||||
timelimit_descriptor, depth=None)
|
||||
timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
|
||||
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"))
|
||||
|
||||
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"))
|
||||
|
||||
# check if we need to provide an accommodation:
|
||||
time_accommodation_mapping = {'ET12ET': 'ADDHALFTIME',
|
||||
'ET30MN': 'ADD30MIN',
|
||||
'ETDBTM': 'ADDDOUBLE', }
|
||||
|
||||
time_accommodation_code = None
|
||||
for code in registration.get_accommodation_codes():
|
||||
if code in time_accommodation_mapping:
|
||||
time_accommodation_code = time_accommodation_mapping[code]
|
||||
|
||||
if time_accommodation_code:
|
||||
timelimit_module.accommodation_code = time_accommodation_code
|
||||
log.info("cand {} on exam {} for course {}: receiving accommodation {}".format(client_candidate_id, exam_series_code, course_id, time_accommodation_code))
|
||||
|
||||
# UGLY HACK!!!
|
||||
# Login assumes that authentication has occurred, and that there is a
|
||||
# backend annotation on the user object, indicating which backend
|
||||
# against which the user was authenticated. We're authenticating here
|
||||
# against the registration entry, and assuming that the request given
|
||||
# this information is correct, we allow the user to be logged in
|
||||
# without a password. This could all be formalized in a backend object
|
||||
# that does the above checking.
|
||||
# TODO: (brian) create a backend class to do this.
|
||||
# testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
|
||||
testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass")
|
||||
login(request, testcenteruser.user)
|
||||
|
||||
# And start the test:
|
||||
return jump_to(request, course_id, location)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1140,132 +1140,6 @@ def accept_name_change(request):
|
||||
|
||||
return accept_name_change_by_id(int(request.POST['id']))
|
||||
|
||||
@csrf_exempt
|
||||
def test_center_login(request):
|
||||
# errors are returned by navigating to the error_url, adding a query parameter named "code"
|
||||
# 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);
|
||||
|
||||
# get provided error URL, which will be used as a known prefix for returning error messages to the
|
||||
# Pearson shell.
|
||||
error_url = request.POST.get("errorURL")
|
||||
|
||||
# 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"));
|
||||
code = request.POST.get("code")
|
||||
|
||||
# calculate SHA for query string
|
||||
# TODO: figure out how to get the original query string, so we can hash it and compare.
|
||||
|
||||
|
||||
if 'clientCandidateID' not in request.POST:
|
||||
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
|
||||
# expected values....
|
||||
# registration_id = request.POST.get("registrationID")
|
||||
# exit_url = request.POST.get("exitURL")
|
||||
|
||||
# find testcenter_user that matches the provided ID:
|
||||
try:
|
||||
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"));
|
||||
|
||||
# find testcenter_registration that matches the provided exam code:
|
||||
# Note that we could rely in future on either the registrationId or the exam code,
|
||||
# or possibly both. But for now we know what to do with an ExamSeriesCode,
|
||||
# while we currently have no record of RegistrationID values at all.
|
||||
if 'vueExamSeriesCode' not in request.POST:
|
||||
# we are not allowed to make up a new error code, according to Pearson,
|
||||
# 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"));
|
||||
exam_series_code = request.POST.get('vueExamSeriesCode')
|
||||
# special case for supporting test user:
|
||||
if client_candidate_id == "edX003671291147" and exam_series_code != '6002x001':
|
||||
log.warning("test user {} using unexpected exam code {}, coercing to 6002x001".format(client_candidate_id, exam_series_code))
|
||||
exam_series_code = '6002x001'
|
||||
|
||||
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"));
|
||||
|
||||
# TODO: figure out what to do if there are more than one registrations....
|
||||
# for now, just take the first...
|
||||
registration = registrations[0]
|
||||
|
||||
course_id = registration.course_id
|
||||
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"));
|
||||
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"));
|
||||
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))
|
||||
|
||||
# check if the test has already been taken
|
||||
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"));
|
||||
|
||||
timelimit_module_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user,
|
||||
timelimit_descriptor, depth=None)
|
||||
timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
|
||||
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"));
|
||||
|
||||
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"));
|
||||
|
||||
# check if we need to provide an accommodation:
|
||||
time_accommodation_mapping = {'ET12ET' : 'ADDHALFTIME',
|
||||
'ET30MN' : 'ADD30MIN',
|
||||
'ETDBTM' : 'ADDDOUBLE', }
|
||||
|
||||
time_accommodation_code = None
|
||||
for code in registration.get_accommodation_codes():
|
||||
if code in time_accommodation_mapping:
|
||||
time_accommodation_code = time_accommodation_mapping[code]
|
||||
# special, hard-coded client ID used by Pearson shell for testing:
|
||||
if client_candidate_id == "edX003671291147":
|
||||
time_accommodation_code = 'TESTING'
|
||||
|
||||
if time_accommodation_code:
|
||||
timelimit_module.accommodation_code = time_accommodation_code
|
||||
log.info("cand {} on exam {} for course {}: receiving accommodation {}".format(client_candidate_id, exam_series_code, course_id, time_accommodation_code))
|
||||
|
||||
# UGLY HACK!!!
|
||||
# Login assumes that authentication has occurred, and that there is a
|
||||
# backend annotation on the user object, indicating which backend
|
||||
# against which the user was authenticated. We're authenticating here
|
||||
# against the registration entry, and assuming that the request given
|
||||
# this information is correct, we allow the user to be logged in
|
||||
# without a password. This could all be formalized in a backend object
|
||||
# that does the above checking.
|
||||
# TODO: (brian) create a backend class to do this.
|
||||
# testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
|
||||
testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass")
|
||||
login(request, testcenteruser.user)
|
||||
|
||||
# And start the test:
|
||||
return jump_to(request, course_id, location)
|
||||
|
||||
|
||||
def _get_news(top=None):
|
||||
"Return the n top news items on settings.RSS_URL"
|
||||
|
||||
@@ -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):
|
||||
'''
|
||||
|
||||
@@ -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$')
|
||||
|
||||
@@ -105,8 +105,12 @@ def add_histogram(get_html, module, user):
|
||||
return get_html()
|
||||
|
||||
module_id = module.id
|
||||
histogram = grade_histogram(module_id)
|
||||
render_histogram = len(histogram) > 0
|
||||
if module.descriptor.has_score:
|
||||
histogram = grade_histogram(module_id)
|
||||
render_histogram = len(histogram) > 0
|
||||
else:
|
||||
histogram = None
|
||||
render_histogram = False
|
||||
|
||||
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
|
||||
[filepath, filename] = getattr(module.descriptor, 'xml_attributes', {}).get('filename', ['', None])
|
||||
|
||||
@@ -24,7 +24,9 @@ default_functions = {'sin': numpy.sin,
|
||||
'arccos': numpy.arccos,
|
||||
'arcsin': numpy.arcsin,
|
||||
'arctan': numpy.arctan,
|
||||
'abs': numpy.abs
|
||||
'abs': numpy.abs,
|
||||
'fact': math.factorial,
|
||||
'factorial': math.factorial
|
||||
}
|
||||
default_variables = {'j': numpy.complex(0, 1),
|
||||
'e': numpy.e,
|
||||
@@ -112,18 +114,18 @@ def evaluator(variables, functions, string, cs=False):
|
||||
return float('nan')
|
||||
|
||||
ops = {"^": operator.pow,
|
||||
"*": operator.mul,
|
||||
"/": operator.truediv,
|
||||
"+": operator.add,
|
||||
"-": operator.sub,
|
||||
}
|
||||
"*": operator.mul,
|
||||
"/": operator.truediv,
|
||||
"+": operator.add,
|
||||
"-": operator.sub,
|
||||
}
|
||||
# We eliminated extreme ones, since they're rarely used, and potentially
|
||||
# confusing. They may also conflict with variables if we ever allow e.g.
|
||||
# 5R instead of 5*R
|
||||
suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9,
|
||||
'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
|
||||
'c': 1e-2, 'm': 1e-3, 'u': 1e-6,
|
||||
'n': 1e-9, 'p': 1e-12} # ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
|
||||
'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
|
||||
'c': 1e-2, 'm': 1e-3, 'u': 1e-6,
|
||||
'n': 1e-9, 'p': 1e-12} # ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
|
||||
|
||||
def super_float(text):
|
||||
''' Like float, but with si extensions. 1k goes to 1000'''
|
||||
@@ -246,4 +248,9 @@ if __name__ == '__main__':
|
||||
print evaluator({}, {}, "5+1*j")
|
||||
print evaluator({}, {}, "j||1")
|
||||
print evaluator({}, {}, "e^(j*pi)")
|
||||
print evaluator({}, {}, "5+7 QWSEKO")
|
||||
print evaluator({}, {}, "fact(5)")
|
||||
print evaluator({}, {}, "factorial(5)")
|
||||
try:
|
||||
print evaluator({}, {}, "5+7 QWSEKO")
|
||||
except UndefinedVariable:
|
||||
print "Successfully caught undefined variable"
|
||||
|
||||
@@ -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):
|
||||
'''
|
||||
@@ -655,6 +668,8 @@ class MatlabInput(CodeInput):
|
||||
# Check if problem has been queued
|
||||
self.queuename = 'matlab'
|
||||
self.queue_msg = ''
|
||||
# this is only set if we don't have a graded response
|
||||
# the graded response takes precedence
|
||||
if 'queue_msg' in self.input_state and self.status in ['queued', 'incomplete', 'unsubmitted']:
|
||||
self.queue_msg = self.input_state['queue_msg']
|
||||
if 'queuestate' in self.input_state and self.input_state['queuestate'] == 'queued':
|
||||
@@ -662,16 +677,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 +694,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 +706,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
|
||||
@@ -699,12 +714,24 @@ class MatlabInput(CodeInput):
|
||||
self.input_state['queuestate'] = None
|
||||
self.input_state['queuekey'] = None
|
||||
|
||||
def button_enabled(self):
|
||||
""" Return whether or not we want the 'Test Code' button visible
|
||||
|
||||
Right now, we only want this button to show up when a problem has not been
|
||||
checked.
|
||||
"""
|
||||
if self.status in ['correct', 'incorrect']:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
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,
|
||||
'button_enabled': self.button_enabled(),
|
||||
}
|
||||
return extra_context
|
||||
|
||||
def _parse_data(self, queue_msg):
|
||||
@@ -719,20 +746,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 +774,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)
|
||||
|
||||
# save the input state
|
||||
self.input_state['queuekey'] = queuekey
|
||||
self.input_state['queuestate'] = 'queued'
|
||||
|
||||
lms_callback_url=callback_url,
|
||||
lms_key=queuekey,
|
||||
queue_name=self.queuename)
|
||||
|
||||
# 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))
|
||||
# save the input state if successful
|
||||
if error == 0:
|
||||
self.input_state['queuekey'] = queuekey
|
||||
self.input_state['queuestate'] = 'queued'
|
||||
|
||||
return {'success': error == 0, 'message': msg}
|
||||
|
||||
@@ -1026,7 +1051,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
|
||||
|
||||
|
||||
@@ -33,9 +33,11 @@
|
||||
${queue_msg|n}
|
||||
</div>
|
||||
|
||||
% if button_enabled:
|
||||
<div class="plot-button">
|
||||
<input type="button" class="save" name="plot-button" id="plot_${id}" value="Plot" />
|
||||
<input type="button" class="save" name="plot-button" id="plot_${id}" value="Run Code" />
|
||||
</div>
|
||||
%endif
|
||||
|
||||
<script>
|
||||
// Note: We need to make the area follow the CodeMirror for this to work.
|
||||
@@ -91,7 +93,7 @@
|
||||
window.location.reload();
|
||||
}
|
||||
else {
|
||||
gentle_alert(problem_elt, msg);
|
||||
gentle_alert(problem_elt, response.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +104,7 @@
|
||||
{'submission': submission}, plot_callback);
|
||||
}
|
||||
else {
|
||||
gentle_alert(problem_elt, msg);
|
||||
gentle_alert(problem_elt, response.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
style="display:none;"
|
||||
% endif
|
||||
/>
|
||||
${trailing_text | h}
|
||||
|
||||
<p class="status">
|
||||
% if status == 'unsubmitted':
|
||||
|
||||
@@ -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'}
|
||||
|
||||
407
common/lib/capa/capa/tests/test_input_templates.py
Normal file
407
common/lib/capa/capa/tests/test_input_templates.py
Normal 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'])
|
||||
@@ -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 < 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,25 @@ 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),
|
||||
'button_enabled': True,
|
||||
'queue_len': '3'}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
def test_rendering_with_state(self):
|
||||
state = {'value': 'print "good evening"',
|
||||
'status': 'incomplete',
|
||||
@@ -366,20 +401,46 @@ 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),
|
||||
'button_enabled': True,
|
||||
'queue_len': '3'}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
def test_rendering_when_completed(self):
|
||||
for status in ['correct', 'incorrect']:
|
||||
state = {'value': 'print "good evening"',
|
||||
'status': status,
|
||||
'input_state': {},
|
||||
}
|
||||
elt = etree.fromstring(self.xml)
|
||||
|
||||
the_input = self.input_class(test_system, elt, state)
|
||||
context = the_input._get_render_context()
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': 'print "good evening"',
|
||||
'status': status,
|
||||
'msg': '',
|
||||
'mode': self.mode,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
'queue_msg': '',
|
||||
'linenumbers': 'true',
|
||||
'hidden': '',
|
||||
'tabsize': int(self.tabsize),
|
||||
'button_enabled': False,
|
||||
'queue_len': '0'}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
def test_rendering_while_queued(self):
|
||||
state = {'value': 'print "good evening"',
|
||||
'status': 'incomplete',
|
||||
@@ -391,17 +452,17 @@ 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),
|
||||
'button_enabled': True,
|
||||
'queue_len': '1'}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
@@ -410,11 +471,22 @@ 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')
|
||||
|
||||
def test_plot_data_failure(self):
|
||||
get = {'submission': 'x = 1234;'}
|
||||
error_message = 'Error message!'
|
||||
test_system.xqueue['interface'].send_to_queue.return_value = (1, error_message)
|
||||
response = self.the_input.handle_ajax("plot", get)
|
||||
self.assertFalse(response['success'])
|
||||
self.assertEqual(response['message'], error_message)
|
||||
self.assertTrue('queuekey' not in self.the_input.input_state)
|
||||
self.assertTrue('queuestate' not in self.the_input.input_state)
|
||||
test_system.xqueue['interface'].send_to_queue.return_value = (0, 'Success!')
|
||||
|
||||
def test_ungraded_response_success(self):
|
||||
queuekey = 'abcd'
|
||||
input_state = {'queuekey': queuekey, 'queuestate': 'queued'}
|
||||
@@ -452,9 +524,6 @@ class MatlabTest(unittest.TestCase):
|
||||
self.assertFalse('queue_msg' in input_state)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class SchematicTest(unittest.TestCase):
|
||||
'''
|
||||
Check that schematic inputs work
|
||||
@@ -468,7 +537,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 +566,7 @@ class SchematicTest(unittest.TestCase):
|
||||
'height': height,
|
||||
'parts': parts,
|
||||
'analyses': analyses,
|
||||
'submit_analyses': submit_analyses,
|
||||
}
|
||||
'submit_analyses': submit_analyses}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
@@ -556,7 +623,6 @@ class ImageInputTest(unittest.TestCase):
|
||||
self.check('[12 13 14]', 0, 0)
|
||||
|
||||
|
||||
|
||||
class CrystallographyTest(unittest.TestCase):
|
||||
'''
|
||||
Check that crystallography inputs work
|
||||
@@ -586,8 +652,7 @@ class CrystallographyTest(unittest.TestCase):
|
||||
'status': 'unsubmitted',
|
||||
'msg': '',
|
||||
'width': width,
|
||||
'height': height,
|
||||
}
|
||||
'height': height}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
@@ -627,13 +692,11 @@ class VseprTest(unittest.TestCase):
|
||||
'width': width,
|
||||
'height': height,
|
||||
'molecules': molecules,
|
||||
'geometries': geometries,
|
||||
}
|
||||
'geometries': geometries}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
|
||||
class ChemicalEquationTest(unittest.TestCase):
|
||||
'''
|
||||
Check that chemical equation inputs work.
|
||||
@@ -647,7 +710,6 @@ class ChemicalEquationTest(unittest.TestCase):
|
||||
state = {'value': 'H2OYeah', }
|
||||
self.the_input = lookup_tag('chemicalequationinput')(test_system, element, state)
|
||||
|
||||
|
||||
def test_rendering(self):
|
||||
''' Verify that the render context matches the expected render context'''
|
||||
context = self.the_input._get_render_context()
|
||||
@@ -661,10 +723,8 @@ class ChemicalEquationTest(unittest.TestCase):
|
||||
}
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
def test_chemcalc_ajax_sucess(self):
|
||||
''' Verify that using the correct dispatch and valid data produces a valid response'''
|
||||
|
||||
data = {'formula': "H"}
|
||||
response = self.the_input.handle_ajax("preview_chemcalc", data)
|
||||
|
||||
@@ -673,9 +733,6 @@ class ChemicalEquationTest(unittest.TestCase):
|
||||
self.assertEqual(response['error'], "")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class DragAndDropTest(unittest.TestCase):
|
||||
'''
|
||||
Check that drag and drop inputs work
|
||||
|
||||
@@ -7,6 +7,7 @@ from datetime import datetime
|
||||
import json
|
||||
from nose.plugins.skip import SkipTest
|
||||
import os
|
||||
import random
|
||||
import unittest
|
||||
import textwrap
|
||||
|
||||
@@ -14,7 +15,7 @@ from . import test_system
|
||||
|
||||
import capa.capa_problem as lcp
|
||||
from capa.responsetypes import LoncapaProblemError, \
|
||||
StudentInputError, ResponseError
|
||||
StudentInputError, ResponseError
|
||||
from capa.correctmap import CorrectMap
|
||||
from capa.util import convert_files_to_filenames
|
||||
from capa.xqueue_interface import dateformat
|
||||
@@ -33,10 +34,13 @@ class ResponseTest(unittest.TestCase):
|
||||
xml = self.xml_factory.build_xml(**kwargs)
|
||||
return lcp.LoncapaProblem(xml, '1', system=test_system)
|
||||
|
||||
def assert_grade(self, problem, submission, expected_correctness):
|
||||
def assert_grade(self, problem, submission, expected_correctness, msg=None):
|
||||
input_dict = {'1_2_1': submission}
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
self.assertEquals(correct_map.get_correctness('1_2_1'), expected_correctness)
|
||||
if msg is None:
|
||||
self.assertEquals(correct_map.get_correctness('1_2_1'), expected_correctness)
|
||||
else:
|
||||
self.assertEquals(correct_map.get_correctness('1_2_1'), expected_correctness, msg)
|
||||
|
||||
def assert_answer_format(self, problem):
|
||||
answers = problem.get_question_answers()
|
||||
@@ -357,6 +361,83 @@ class FormulaResponseTest(ResponseTest):
|
||||
self.assert_grade(problem, '2*x', 'correct')
|
||||
self.assert_grade(problem, '3*x', 'incorrect')
|
||||
|
||||
def test_parallel_resistors(self):
|
||||
"""Test parallel resistors"""
|
||||
sample_dict = {'R1': (10, 10), 'R2': (2, 2), 'R3': (5, 5), 'R4': (1, 1)}
|
||||
|
||||
# Test problem
|
||||
problem = self.build_problem(sample_dict=sample_dict,
|
||||
num_samples=10,
|
||||
tolerance=0.01,
|
||||
answer="R1||R2")
|
||||
# Expect answer to be marked correct
|
||||
input_formula = "R1||R2"
|
||||
self.assert_grade(problem, input_formula, "correct")
|
||||
|
||||
# Expect random number to be marked incorrect
|
||||
input_formula = "13"
|
||||
self.assert_grade(problem, input_formula, "incorrect")
|
||||
|
||||
# Expect incorrect answer marked incorrect
|
||||
input_formula = "R3||R4"
|
||||
self.assert_grade(problem, input_formula, "incorrect")
|
||||
|
||||
def test_default_variables(self):
|
||||
"""Test the default variables provided in common/lib/capa/capa/calc.py"""
|
||||
# which are: j (complex number), e, pi, k, c, T, q
|
||||
|
||||
# Sample x in the range [-10,10]
|
||||
sample_dict = {'x': (-10, 10)}
|
||||
default_variables = [('j', 2, 3), ('e', 2, 3), ('pi', 2, 3), ('c', 2, 3), ('T', 2, 3),
|
||||
('k', 2 * 10 ** 23, 3 * 10 ** 23), # note k = scipy.constants.k = 1.3806488e-23
|
||||
('q', 2 * 10 ** 19, 3 * 10 ** 19)] # note k = scipy.constants.e = 1.602176565e-19
|
||||
for (var, cscalar, iscalar) in default_variables:
|
||||
# The expected solution is numerically equivalent to cscalar*var
|
||||
correct = '{0}*x*{1}'.format(cscalar, var)
|
||||
incorrect = '{0}*x*{1}'.format(iscalar, var)
|
||||
problem = self.build_problem(sample_dict=sample_dict,
|
||||
num_samples=10,
|
||||
tolerance=0.01,
|
||||
answer=correct)
|
||||
|
||||
# Expect that the inputs are graded correctly
|
||||
self.assert_grade(problem, correct, 'correct',
|
||||
msg="Failed on variable {0}; the given, correct answer was {1} but graded 'incorrect'".format(var, correct))
|
||||
self.assert_grade(problem, incorrect, 'incorrect',
|
||||
msg="Failed on variable {0}; the given, incorrect answer was {1} but graded 'correct'".format(var, incorrect))
|
||||
|
||||
def test_default_functions(self):
|
||||
"""Test the default functions provided in common/lib/capa/capa/calc.py"""
|
||||
# which are: sin, cos, tan, sqrt, log10, log2, ln,
|
||||
# arccos, arcsin, arctan, abs,
|
||||
# fact, factorial
|
||||
|
||||
w = random.randint(3, 10)
|
||||
sample_dict = {'x': (-10, 10), # Sample x in the range [-10,10]
|
||||
'y': (1, 10), # Sample y in the range [1,10] - logs, arccos need positive inputs
|
||||
'z': (-1, 1), # Sample z in the range [1,10] - for arcsin, arctan
|
||||
'w': (w, w)} # Sample w is a random, positive integer - factorial needs a positive, integer input,
|
||||
# and the way formularesponse is defined, we can only specify a float range
|
||||
|
||||
default_functions = [('sin', 2, 3, 'x'), ('cos', 2, 3, 'x'), ('tan', 2, 3, 'x'), ('sqrt', 2, 3, 'y'), ('log10', 2, 3, 'y'),
|
||||
('log2', 2, 3, 'y'), ('ln', 2, 3, 'y'), ('arccos', 2, 3, 'z'), ('arcsin', 2, 3, 'z'), ('arctan', 2, 3, 'x'),
|
||||
('abs', 2, 3, 'x'), ('fact', 2, 3, 'w'), ('factorial', 2, 3, 'w')]
|
||||
for (func, cscalar, iscalar, var) in default_functions:
|
||||
print 'func is: {0}'.format(func)
|
||||
# The expected solution is numerically equivalent to cscalar*func(var)
|
||||
correct = '{0}*{1}({2})'.format(cscalar, func, var)
|
||||
incorrect = '{0}*{1}({2})'.format(iscalar, func, var)
|
||||
problem = self.build_problem(sample_dict=sample_dict,
|
||||
num_samples=10,
|
||||
tolerance=0.01,
|
||||
answer=correct)
|
||||
|
||||
# Expect that the inputs are graded correctly
|
||||
self.assert_grade(problem, correct, 'correct',
|
||||
msg="Failed on function {0}; the given, correct answer was {1} but graded 'incorrect'".format(func, correct))
|
||||
self.assert_grade(problem, incorrect, 'incorrect',
|
||||
msg="Failed on function {0}; the given, incorrect answer was {1} but graded 'correct'".format(func, incorrect))
|
||||
|
||||
|
||||
class StringResponseTest(ResponseTest):
|
||||
from response_xml_factory import StringResponseXMLFactory
|
||||
@@ -904,14 +985,13 @@ class CustomResponseTest(ResponseTest):
|
||||
with self.assertRaises(ResponseError):
|
||||
problem.grade_answers({'1_2_1': '42'})
|
||||
|
||||
|
||||
def test_module_imports_inline(self):
|
||||
'''
|
||||
Check that the correct modules are available to custom
|
||||
response scripts
|
||||
'''
|
||||
|
||||
for module_name in ['random', 'numpy', 'math', 'scipy',
|
||||
for module_name in ['random', 'numpy', 'math', 'scipy',
|
||||
'calc', 'eia', 'chemcalc', 'chemtools',
|
||||
'miller', 'draganddrop']:
|
||||
|
||||
@@ -921,26 +1001,25 @@ class CustomResponseTest(ResponseTest):
|
||||
script = textwrap.dedent('''
|
||||
correct[0] = 'correct'
|
||||
assert('%s' in globals())''' % module_name)
|
||||
|
||||
|
||||
# Create the problem
|
||||
problem = self.build_problem(answer=script)
|
||||
|
||||
# Expect that we can grade an answer without
|
||||
# Expect that we can grade an answer without
|
||||
# getting an exception
|
||||
try:
|
||||
problem.grade_answers({'1_2_1': '42'})
|
||||
|
||||
except ResponseError:
|
||||
self.fail("Could not use name '%s' in custom response"
|
||||
% module_name)
|
||||
|
||||
self.fail("Could not use name '{0}s' in custom response".format(module_name))
|
||||
|
||||
def test_module_imports_function(self):
|
||||
'''
|
||||
Check that the correct modules are available to custom
|
||||
response scripts
|
||||
'''
|
||||
|
||||
for module_name in ['random', 'numpy', 'math', 'scipy',
|
||||
for module_name in ['random', 'numpy', 'math', 'scipy',
|
||||
'calc', 'eia', 'chemcalc', 'chemtools',
|
||||
'miller', 'draganddrop']:
|
||||
|
||||
@@ -951,18 +1030,17 @@ class CustomResponseTest(ResponseTest):
|
||||
def check_func(expect, answer_given):
|
||||
assert('%s' in globals())
|
||||
return True''' % module_name)
|
||||
|
||||
|
||||
# Create the problem
|
||||
problem = self.build_problem(script=script, cfn="check_func")
|
||||
|
||||
# Expect that we can grade an answer without
|
||||
# Expect that we can grade an answer without
|
||||
# getting an exception
|
||||
try:
|
||||
problem.grade_answers({'1_2_1': '42'})
|
||||
|
||||
except ResponseError:
|
||||
self.fail("Could not use name '%s' in custom response"
|
||||
% module_name)
|
||||
self.fail("Could not use name '{0}s' in custom response".format(module_name))
|
||||
|
||||
|
||||
class SchematicResponseTest(ResponseTest):
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -8,7 +8,7 @@ def rooted_glob(root, glob):
|
||||
|
||||
Uses glob2 globbing
|
||||
"""
|
||||
return remove_root(root, glob2.glob('{root}/{glob}'.format(root=root, glob=glob)))
|
||||
return remove_root(root, sorted(glob2.glob('{root}/{glob}'.format(root=root, glob=glob))))
|
||||
|
||||
|
||||
def remove_root(root, paths):
|
||||
|
||||
@@ -4,13 +4,15 @@ setup(
|
||||
name="XModule",
|
||||
version="0.1",
|
||||
packages=find_packages(exclude=["tests"]),
|
||||
install_requires=['distribute'],
|
||||
install_requires=[
|
||||
'distribute',
|
||||
'docopt',
|
||||
'capa',
|
||||
'path.py',
|
||||
],
|
||||
package_data={
|
||||
'xmodule': ['js/module/*']
|
||||
},
|
||||
requires=[
|
||||
'capa',
|
||||
],
|
||||
|
||||
# See http://guide.python-distribute.org/creation.html#entry-points
|
||||
# for a description of entry_points
|
||||
@@ -50,6 +52,11 @@ setup(
|
||||
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
|
||||
"annotatable = xmodule.annotatable_module:AnnotatableDescriptor",
|
||||
"foldit = xmodule.foldit_module:FolditDescriptor",
|
||||
]
|
||||
"hidden = xmodule.hidden_module:HiddenDescriptor",
|
||||
"raw = xmodule.raw_module:RawDescriptor",
|
||||
],
|
||||
'console_scripts': [
|
||||
'xmodule_assets = xmodule.static_content:main',
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -16,35 +16,13 @@ from .progress import Progress
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from xblock.core import Integer, Scope, String, Boolean, Object, Float
|
||||
from .fields import Timedelta, Date
|
||||
from xblock.core import Scope, String, Boolean, Object
|
||||
from .fields import Timedelta, Date, StringyInteger, StringyFloat
|
||||
from xmodule.util.date_utils import time_to_datetime
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
|
||||
class StringyInteger(Integer):
|
||||
"""
|
||||
A model type that converts from strings to integers when reading from json
|
||||
"""
|
||||
def from_json(self, value):
|
||||
try:
|
||||
return int(value)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
class StringyFloat(Float):
|
||||
"""
|
||||
A model type that converts from string to floats when reading from json
|
||||
"""
|
||||
def from_json(self, value):
|
||||
try:
|
||||
return float(value)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
# Generated this many different variants of problems with rerandomize=per_student
|
||||
NUM_RANDOMIZATION_BINS = 20
|
||||
|
||||
@@ -95,7 +73,6 @@ class CapaFields(object):
|
||||
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state)
|
||||
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state)
|
||||
done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state)
|
||||
display_name = String(help="Display name for this module", scope=Scope.settings)
|
||||
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)
|
||||
|
||||
@@ -8,13 +8,12 @@ from .x_module import XModule
|
||||
from xblock.core import Integer, Scope, String, Boolean, List
|
||||
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
|
||||
from collections import namedtuple
|
||||
from .fields import Date
|
||||
from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
|
||||
from .fields import Date, 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 +218,5 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
always_recalculate_grades=True
|
||||
always_recalculate_grades = True
|
||||
template_dir_name = "combinedopenended"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import absolute_import
|
||||
from importlib import import_module
|
||||
from os import environ
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -122,6 +122,7 @@ div.combined-rubric-container {
|
||||
|
||||
span.rubric-category {
|
||||
font-size: .9em;
|
||||
font-weight: bold;
|
||||
}
|
||||
padding-bottom: 5px;
|
||||
padding-top: 10px;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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
|
||||
@@ -16,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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -7,6 +7,8 @@ from xblock.core import ModelType
|
||||
import datetime
|
||||
import dateutil.parser
|
||||
|
||||
from xblock.core import Integer, Float, Boolean
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -51,6 +53,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):
|
||||
"""
|
||||
@@ -79,3 +83,42 @@ class Timedelta(ModelType):
|
||||
if cur_value > 0:
|
||||
values.append("%d %s" % (cur_value, attr))
|
||||
return ' '.join(values)
|
||||
|
||||
|
||||
class StringyInteger(Integer):
|
||||
"""
|
||||
A model type that converts from strings to integers when reading from json.
|
||||
If value does not parse as an int, returns None.
|
||||
"""
|
||||
def from_json(self, value):
|
||||
try:
|
||||
return int(value)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
class StringyFloat(Float):
|
||||
"""
|
||||
A model type that converts from string to floats when reading from json.
|
||||
If value does not parse as a float, returns None.
|
||||
"""
|
||||
def from_json(self, value):
|
||||
try:
|
||||
return float(value)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
class StringyBoolean(Boolean):
|
||||
"""
|
||||
Reads strings from JSON as booleans.
|
||||
|
||||
If the string is 'true' (case insensitive), then return True,
|
||||
otherwise False.
|
||||
|
||||
JSON values that aren't strings are returned as-is.
|
||||
"""
|
||||
def from_json(self, value):
|
||||
if isinstance(value, basestring):
|
||||
return value.lower() == 'true'
|
||||
return value
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -37,7 +37,7 @@ xdescribe 'VideoPlayer', ->
|
||||
expect(window.VideoProgressSlider).toHaveBeenCalledWith el: $('.slider', @player.el)
|
||||
|
||||
it 'create Youtube player', ->
|
||||
expect(YT.Player).toHaveBeenCalledWith 'example'
|
||||
expect(YT.Player).toHaveBeenCalledWith('example', {
|
||||
playerVars:
|
||||
controls: 0
|
||||
wmode: 'transparent'
|
||||
@@ -48,6 +48,7 @@ xdescribe 'VideoPlayer', ->
|
||||
events:
|
||||
onReady: @player.onReady
|
||||
onStateChange: @player.onStateChange
|
||||
})
|
||||
|
||||
it 'bind to video control play event', ->
|
||||
expect($(@player.control)).toHandleWith 'play', @player.play
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -13,6 +13,12 @@ def as_draft(location):
|
||||
"""
|
||||
return Location(location)._replace(revision=DRAFT)
|
||||
|
||||
def as_published(location):
|
||||
"""
|
||||
Returns the Location that is the published version for `location`
|
||||
"""
|
||||
return Location(location)._replace(revision=None)
|
||||
|
||||
|
||||
def wrap_draft(item):
|
||||
"""
|
||||
@@ -159,13 +165,17 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
|
||||
return super(DraftModuleStore, self).update_metadata(draft_loc, metadata)
|
||||
|
||||
def delete_item(self, location):
|
||||
def delete_item(self, location, delete_all_versions=False):
|
||||
"""
|
||||
Delete an item from this modulestore
|
||||
|
||||
location: Something that can be passed to Location
|
||||
"""
|
||||
return super(DraftModuleStore, self).delete_item(as_draft(location))
|
||||
super(DraftModuleStore, self).delete_item(as_draft(location))
|
||||
if delete_all_versions:
|
||||
super(DraftModuleStore, self).delete_item(as_published(location))
|
||||
|
||||
return
|
||||
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,6 @@ 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
|
||||
|
||||
@@ -31,11 +30,13 @@ log = logging.getLogger(__name__)
|
||||
# there is only one revision for each item. Once we start versioning inside the CMS,
|
||||
# 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
|
||||
@@ -130,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
|
||||
@@ -140,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
|
||||
@@ -203,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
|
||||
@@ -221,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)
|
||||
@@ -466,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):
|
||||
'''
|
||||
@@ -692,11 +694,12 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
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):
|
||||
def delete_item(self, location, delete_all_versions=False):
|
||||
"""
|
||||
Delete an item from this modulestore
|
||||
|
||||
location: Something that can be passed to Location
|
||||
delete_all_versions: is here because the DraftMongoModuleStore needs it and we need to keep the interface the same. It is unused.
|
||||
"""
|
||||
# VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
|
||||
# if we add one then we need to also add it to the policy information (i.e. metadata)
|
||||
@@ -708,10 +711,9 @@ 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))
|
||||
@@ -722,7 +724,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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.mongo import MongoModuleStore
|
||||
@@ -14,10 +13,19 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
if not modulestore.has_item(dest_location):
|
||||
raise Exception("An empty course at {0} must have already been created. Aborting...".format(dest_location))
|
||||
|
||||
# verify that the dest_location really is an empty course, which means only one
|
||||
# verify that the dest_location really is an empty course, which means only one with an optional 'overview'
|
||||
dest_modules = modulestore.get_items([dest_location.tag, dest_location.org, dest_location.course, None, None, None])
|
||||
|
||||
if len(dest_modules) != 1:
|
||||
basically_empty = True
|
||||
for module in dest_modules:
|
||||
if module.location.category == 'course' or (module.location.category == 'about'
|
||||
and module.location.name == 'overview'):
|
||||
continue
|
||||
|
||||
basically_empty = False
|
||||
break
|
||||
|
||||
if not basically_empty:
|
||||
raise Exception("Course at destination {0} is not an empty course. You can only clone into an empty course. Aborting...".format(dest_location))
|
||||
|
||||
# check to see if the source course is actually there
|
||||
@@ -33,11 +41,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 +57,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 +75,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 +88,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 +102,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....")
|
||||
|
||||
104
common/lib/xmodule/xmodule/modulestore/tests/django_utils.py
Normal file
104
common/lib/xmodule/xmodule/modulestore/tests/django_utils.py
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -50,12 +50,14 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
|
||||
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)
|
||||
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)
|
||||
# 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=''):
|
||||
|
||||
@@ -274,7 +274,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
|
||||
# now import any 'draft' items
|
||||
if draft_store is not None:
|
||||
import_course_draft(xml_module_store, draft_store, course_data_path,
|
||||
import_course_draft(xml_module_store, store, draft_store, course_data_path,
|
||||
static_content_store, target_location_namespace if target_location_namespace is not None
|
||||
else course_location)
|
||||
|
||||
@@ -316,7 +316,7 @@ def import_module(module, store, course_data_path, static_content_store, allow_n
|
||||
# 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])
|
||||
@@ -339,7 +339,7 @@ def import_module(module, store, course_data_path, static_content_store, allow_n
|
||||
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):
|
||||
def import_course_draft(xml_module_store, store, draft_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)
|
||||
@@ -396,7 +396,7 @@ def import_course_draft(xml_module_store, store, course_data_path, static_conten
|
||||
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)
|
||||
import_module(module, draft_store, course_data_path, static_content_store, allow_not_found=True)
|
||||
for child in module.get_children():
|
||||
_import_module(child)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user