diff --git a/.gitignore b/.gitignore index 8fb170c30f..d01baf055a 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.pylintrc b/.pylintrc index 49fcf80eb9..792079ce03 100644 --- a/.pylintrc +++ b/.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] diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 0000000000..93a8706d3e --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +mitx diff --git a/apt-packages.txt b/apt-packages.txt index 0560dfcbc2..2635388757 100644 --- a/apt-packages.txt +++ b/apt-packages.txt @@ -22,5 +22,4 @@ libreadline6 libreadline6-dev mongodb nodejs -npm coffeescript diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py index 281e3f46b2..71b5e97bc2 100644 --- a/cms/djangoapps/auth/authz.py +++ b/cms/djangoapps/auth/authz.py @@ -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) diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py index 589db4ac56..ada3873992 100644 --- a/cms/djangoapps/contentstore/course_info_model.py +++ b/cms/djangoapps/contentstore/course_info_model.py @@ -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'], diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index 558294e890..ca5b62e596 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -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 diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 6fb102faea..ea5b24b21f 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -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"']) diff --git a/cms/djangoapps/contentstore/features/checklists.feature b/cms/djangoapps/contentstore/features/checklists.feature index bccb80b8d7..ddf1adf263 100644 --- a/cms/djangoapps/contentstore/features/checklists.feature +++ b/cms/djangoapps/contentstore/features/checklists.feature @@ -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 - diff --git a/cms/djangoapps/contentstore/features/checklists.py b/cms/djangoapps/contentstore/features/checklists.py index dc399f5fac..d433dbbf0d 100644 --- a/cms/djangoapps/contentstore/features/checklists.py +++ b/cms/djangoapps/contentstore/features/checklists.py @@ -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() - diff --git a/cms/djangoapps/contentstore/features/course-settings.feature b/cms/djangoapps/contentstore/features/course-settings.feature index e869bfe47a..fc9641cb46 100644 --- a/cms/djangoapps/contentstore/features/course-settings.feature +++ b/cms/djangoapps/contentstore/features/course-settings.feature @@ -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 diff --git a/cms/djangoapps/contentstore/features/section.feature b/cms/djangoapps/contentstore/features/section.feature index 08d38367bc..24cbeb3db9 100644 --- a/cms/djangoapps/contentstore/features/section.feature +++ b/cms/djangoapps/contentstore/features/section.feature @@ -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 diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature index 762dea6838..a0e0a48f9e 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature @@ -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 \ No newline at end of file + And all sections are expanded diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature index cc3b2b1cbb..28285bf8a1 100644 --- a/cms/djangoapps/contentstore/features/subsection.feature +++ b/cms/djangoapps/contentstore/features/subsection.feature @@ -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 - - diff --git a/cms/djangoapps/contentstore/management/commands/check_course.py b/cms/djangoapps/contentstore/management/commands/check_course.py index 57965fe793..215bb8add8 100644 --- a/cms/djangoapps/contentstore/management/commands/check_course.py +++ b/cms/djangoapps/contentstore/management/commands/check_course.py @@ -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: diff --git a/cms/djangoapps/contentstore/management/commands/clone.py b/cms/djangoapps/contentstore/management/commands/clone.py index abf04f3da3..0ca50acb50 100644 --- a/cms/djangoapps/contentstore/management/commands/clone.py +++ b/cms/djangoapps/contentstore/management/commands/clone.py @@ -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: diff --git a/cms/djangoapps/contentstore/management/commands/delete_course.py b/cms/djangoapps/contentstore/management/commands/delete_course.py index fc92205030..5aafe9f8a6 100644 --- a/cms/djangoapps/contentstore/management/commands/delete_course.py +++ b/cms/djangoapps/contentstore/management/commands/delete_course.py @@ -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: diff --git a/cms/djangoapps/contentstore/management/commands/export.py b/cms/djangoapps/contentstore/management/commands/export.py index 11b043c2ab..eb7800d46c 100644 --- a/cms/djangoapps/contentstore/management/commands/export.py +++ b/cms/djangoapps/contentstore/management/commands/export.py @@ -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: diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 2a040f35b6..9b919daad0 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -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) diff --git a/cms/djangoapps/contentstore/management/commands/prompt.py b/cms/djangoapps/contentstore/management/commands/prompt.py index 40a39d0a11..44f981b5ac 100644 --- a/cms/djangoapps/contentstore/management/commands/prompt.py +++ b/cms/djangoapps/contentstore/management/commands/prompt.py @@ -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") diff --git a/cms/djangoapps/contentstore/management/commands/update_templates.py b/cms/djangoapps/contentstore/management/commands/update_templates.py index b30d30480a..e94fee64b8 100644 --- a/cms/djangoapps/contentstore/management/commands/update_templates.py +++ b/cms/djangoapps/contentstore/management/commands/update_templates.py @@ -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() \ No newline at end of file + update_templates() diff --git a/cms/djangoapps/contentstore/management/commands/xlint.py b/cms/djangoapps/contentstore/management/commands/xlint.py index 6bc254a1ff..21c8e7d1f8 100644 --- a/cms/djangoapps/contentstore/management/commands/xlint.py +++ b/cms/djangoapps/contentstore/management/commands/xlint.py @@ -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: [...]") diff --git a/cms/djangoapps/contentstore/module_info_model.py b/cms/djangoapps/contentstore/module_info_model.py index 8ea6add88d..91f722a699 100644 --- a/cms/djangoapps/contentstore/module_info_model.py +++ b/cms/djangoapps/contentstore/module_info_model.py @@ -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): diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index faec60f3e8..07b7032e60 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -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, '

My Courses

', 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, 'Robot Super Educational Course', 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, '
', 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.""" diff --git a/cms/djangoapps/contentstore/tests/test_core_caching.py b/cms/djangoapps/contentstore/tests/test_core_caching.py index 676627a045..34ed24699d 100644 --- a/cms/djangoapps/contentstore/tests/test_core_caching.py +++ b/cms/djangoapps/contentstore/tests/test_core_caching.py @@ -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') diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index fe90ad18aa..c9f6b2053e 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -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 = "bar" # 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') diff --git a/cms/djangoapps/contentstore/tests/test_i18n.py b/cms/djangoapps/contentstore/tests/test_i18n.py new file mode 100644 index 0000000000..e6d68ba004 --- /dev/null +++ b/cms/djangoapps/contentstore/tests/test_i18n.py @@ -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, + '

My Courses

', + 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, + '

My Courses

', + 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'

' \ + + u'My \xc7\xf6\xfcrs\xe9s L#' \ + + u'

' + + self.assertContains(resp, + TEST_STRING, + status_code=200, + html=True) diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py index bbaebfb687..eb7bfb6db9 100644 --- a/cms/djangoapps/contentstore/tests/test_utils.py +++ b/cms/djangoapps/contentstore/tests/test_utils.py @@ -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) - ) \ No newline at end of file + ) diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index e43a95fccd..34e5da4b4d 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -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() diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index bb7ac2bf06..3135e49a08 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -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""" diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index ec439b3312..a5a3b47bce 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -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 diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 3169b437ed..caf3901e03 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -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) diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index 876000c7fe..0dbb47b31b 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -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): diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py index ee9b4ac0eb..4ea9f2f5db 100644 --- a/cms/djangoapps/models/settings/course_grading.py +++ b/cms/djangoapps/models/settings/course_grading.py @@ -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 diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index 70f69315ff..4429e35692 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -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) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 26a8adc92c..1e7a32dc68 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -36,3 +36,4 @@ DATABASES = { INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('contentstore',) LETTUCE_SERVER_PORT = 8001 +LETTUCE_BROWSER = 'chrome' diff --git a/cms/envs/aws.py b/cms/envs/aws.py index edf67badfe..59ad8b835e 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -67,4 +67,4 @@ MODULESTORE = AUTH_TOKENS['MODULESTORE'] CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE'] # Datadog for events! -DATADOG_API = AUTH_TOKENS.get("DATADOG_API") \ No newline at end of file +DATADOG_API = AUTH_TOKENS.get("DATADOG_API") diff --git a/cms/envs/common.py b/cms/envs/common.py index 37cfeea7a1..8effc773e0 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -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 ##################################### diff --git a/cms/envs/jasmine.py b/cms/envs/jasmine.py index 5c9be1cf9c..e046a6d37c 100644 --- a/cms/envs/jasmine.py +++ b/cms/envs/jasmine.py @@ -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', ) diff --git a/cms/envs/test.py b/cms/envs/test.py index 820b2cbe23..63b5efc645 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -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 + diff --git a/cms/static/coffee/.gitignore b/cms/static/coffee/.gitignore index e114474f98..a6c7c2852d 100644 --- a/cms/static/coffee/.gitignore +++ b/cms/static/coffee/.gitignore @@ -1,3 +1 @@ *.js -descriptor -module diff --git a/cms/static/coffee/files.json b/cms/static/coffee/files.json index e7a66b5bc0..c2e1a8acf6 100644 --- a/cms/static/coffee/files.json +++ b/cms/static/coffee/files.json @@ -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" ] } diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 51d358d0eb..3a51d797ec 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.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 tar.gz extension.').show(); + $('.error-block').html(gettext('File format not supported. Please upload a file with a tar.gz 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('Will Release: ' + input_date + ' at ' + input_time + ' UTCEdit'); + var format = gettext('Will Release: %(date)s at $(time)s UTC'); + var willReleaseAt = interpolate(format, [input_date, input_time], true); + $thisSection.find('.section-published-date').html( + '' + willReleaseAt + '' + + '' + + gettext('Edit') + ''); $thisSection.find('.section-published-date').animate({ 'background-color': 'rgb(182,37,104)' }, 300).animate({ diff --git a/cms/static/sass/.gitignore b/cms/static/sass/.gitignore index 62a745a9d7..b3a5267117 100644 --- a/cms/static/sass/.gitignore +++ b/cms/static/sass/.gitignore @@ -1,3 +1 @@ *.css -descriptor -module diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index a355b3a03f..ee6ff18d43 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -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'; diff --git a/cms/static/sass/bourbon b/cms/static/sass/bourbon deleted file mode 120000 index 6f53a8b404..0000000000 --- a/cms/static/sass/bourbon +++ /dev/null @@ -1 +0,0 @@ -../../../common/static/sass/bourbon/ \ No newline at end of file diff --git a/cms/templates/base.html b/cms/templates/base.html index 4517790622..f1a87d6424 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -30,6 +30,7 @@ <%include file="courseware_vendor_js.html"/> + diff --git a/cms/templates/index.html b/cms/templates/index.html index 916720f4e7..0f6e982b1d 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -1,6 +1,8 @@ +<%! from django.utils.translation import ugettext as _ %> + <%inherit file="base.html" /> -<%block name="title">My Courses +<%block name="title">${_("My Courses")} <%block name="bodyclass">is-signedin index dashboard <%block name="header_extras"> @@ -36,18 +38,18 @@
-

My Courses

+

${_("My Courses")}

% if user.is_active:
- \ No newline at end of file + diff --git a/cms/templates/settings.html b/cms/templates/settings.html index cc5dafc57b..0a647c632e 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -87,12 +87,12 @@ from contentstore import utils @@ -179,7 +179,7 @@ from contentstore import utils
  • - Introductions, prerequisites, FAQs that are used on your course summary page + Introductions, prerequisites, FAQs that are used on your course summary page (formatted in HTML)
  • diff --git a/cms/templates/settings_graders.html b/cms/templates/settings_graders.html index 86be66c950..2e98409585 100644 --- a/cms/templates/settings_graders.html +++ b/cms/templates/settings_graders.html @@ -4,20 +4,20 @@ <%namespace name='static' file='static_content.html'/> <%! -from contentstore import utils +from contentstore import utils %> <%block name="jsextra"> - + - + @@ -97,7 +97,7 @@ from contentstore import utils
    1. - + Leeway on due dates
    @@ -112,13 +112,13 @@ from contentstore import utils
      - -
    + + diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index 7162dad50f..db7d5fb3f8 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -1,8 +1,10 @@ <%! from django.core.urlresolvers import reverse %> +<%! from django.utils.translation import ugettext as _ %> +
  • --> % if user.is_authenticated(): % endif - \ No newline at end of file + + diff --git a/cms/urls.py b/cms/urls.py index 06569e4178..30d9ccbf56 100644 --- a/cms/urls.py +++ b/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')),) diff --git a/cms/xmodule_namespace.py b/cms/xmodule_namespace.py index c9bb8f4c6e..1b509a14f4 100644 --- a/cms/xmodule_namespace.py +++ b/cms/xmodule_namespace.py @@ -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): diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index effae923b3..23b46aa803 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -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) diff --git a/common/djangoapps/student/tests/factories.py b/common/djangoapps/student/tests/factories.py index f74188725a..0d9621fc01 100644 --- a/common/djangoapps/student/tests/factories.py +++ b/common/djangoapps/student/tests/factories.py @@ -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' diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 615d72b391..645c1249e0 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -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" diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index c8cc0c9e4b..1d371a3242 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -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): ''' diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index a2db80712f..fdab514177 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -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$') diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py index 3f6db354d6..45691cd854 100644 --- a/common/djangoapps/xmodule_modifiers.py +++ b/common/djangoapps/xmodule_modifiers.py @@ -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]) diff --git a/common/lib/capa/capa/calc.py b/common/lib/capa/capa/calc.py index c3fe6b656b..bb1fb97153 100644 --- a/common/lib/capa/capa/calc.py +++ b/common/lib/capa/capa/calc.py @@ -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" diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 18bc92f0a3..e253b61948 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -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: + + + 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 diff --git a/common/lib/capa/capa/templates/matlabinput.html b/common/lib/capa/capa/templates/matlabinput.html index 6c02e8e68e..69e412f43e 100644 --- a/common/lib/capa/capa/templates/matlabinput.html +++ b/common/lib/capa/capa/templates/matlabinput.html @@ -33,9 +33,11 @@ ${queue_msg|n} + % if button_enabled:
    - +
    + %endif + + + \ No newline at end of file diff --git a/common/static/js/capa/genex/46DBCB09BEC38A6DEE76494C6517111B.cache.html b/common/static/js/capa/genex/46DBCB09BEC38A6DEE76494C6517111B.cache.html deleted file mode 100644 index 054ef6c31a..0000000000 --- a/common/static/js/capa/genex/46DBCB09BEC38A6DEE76494C6517111B.cache.html +++ /dev/null @@ -1,641 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/genex/557C7018CDCA52B163256408948A1722.cache.html b/common/static/js/capa/genex/557C7018CDCA52B163256408948A1722.cache.html deleted file mode 100644 index 3862093b1b..0000000000 --- a/common/static/js/capa/genex/557C7018CDCA52B163256408948A1722.cache.html +++ /dev/null @@ -1,651 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/genex/63308EE54E8033A708B414CAC05B0C32.cache.html b/common/static/js/capa/genex/63308EE54E8033A708B414CAC05B0C32.cache.html new file mode 100644 index 0000000000..952e3b5f37 --- /dev/null +++ b/common/static/js/capa/genex/63308EE54E8033A708B414CAC05B0C32.cache.html @@ -0,0 +1,642 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/genex/7AC57DC6EC8C1D8672DDF6E6D4EF57CC.cache.html b/common/static/js/capa/genex/7AC57DC6EC8C1D8672DDF6E6D4EF57CC.cache.html new file mode 100644 index 0000000000..95cb962805 --- /dev/null +++ b/common/static/js/capa/genex/7AC57DC6EC8C1D8672DDF6E6D4EF57CC.cache.html @@ -0,0 +1,628 @@ + + + \ No newline at end of file diff --git a/common/static/js/capa/genex/866AF633CAA7EA4DA7E906456CDEC65A.cache.html b/common/static/js/capa/genex/866AF633CAA7EA4DA7E906456CDEC65A.cache.html deleted file mode 100644 index 472502dde2..0000000000 --- a/common/static/js/capa/genex/866AF633CAA7EA4DA7E906456CDEC65A.cache.html +++ /dev/null @@ -1,627 +0,0 @@ - - - \ No newline at end of file diff --git a/common/static/js/capa/genex/8F9C3F1A91187AA8391FD08BA7F8716D.cache.html b/common/static/js/capa/genex/8F9C3F1A91187AA8391FD08BA7F8716D.cache.html deleted file mode 100644 index f488b6fcf6..0000000000 --- a/common/static/js/capa/genex/8F9C3F1A91187AA8391FD08BA7F8716D.cache.html +++ /dev/null @@ -1,641 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/genex/9B4F4D4EFA24CDE2E4287CC07897F249.cache.html b/common/static/js/capa/genex/9B4F4D4EFA24CDE2E4287CC07897F249.cache.html new file mode 100644 index 0000000000..5c828c1209 --- /dev/null +++ b/common/static/js/capa/genex/9B4F4D4EFA24CDE2E4287CC07897F249.cache.html @@ -0,0 +1,654 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/genex/A016796CF7FB22261AE1160531B5CF82.cache.html b/common/static/js/capa/genex/A016796CF7FB22261AE1160531B5CF82.cache.html deleted file mode 100644 index f799ecf5b7..0000000000 --- a/common/static/js/capa/genex/A016796CF7FB22261AE1160531B5CF82.cache.html +++ /dev/null @@ -1,653 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/genex/A069AC107D79C29D6237614AC340F0C0.cache.html b/common/static/js/capa/genex/A069AC107D79C29D6237614AC340F0C0.cache.html new file mode 100644 index 0000000000..bcf15330d9 --- /dev/null +++ b/common/static/js/capa/genex/A069AC107D79C29D6237614AC340F0C0.cache.html @@ -0,0 +1,652 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/genex/C6220FCC8B9234FEAD8D826A73C6D2A4.cache.html b/common/static/js/capa/genex/C6220FCC8B9234FEAD8D826A73C6D2A4.cache.html new file mode 100644 index 0000000000..5ab12af718 --- /dev/null +++ b/common/static/js/capa/genex/C6220FCC8B9234FEAD8D826A73C6D2A4.cache.html @@ -0,0 +1,642 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/genex/F28D6C3D881F6C18E3357AAB004477EF.cache.html b/common/static/js/capa/genex/F28D6C3D881F6C18E3357AAB004477EF.cache.html deleted file mode 100644 index 0c242fde9c..0000000000 --- a/common/static/js/capa/genex/F28D6C3D881F6C18E3357AAB004477EF.cache.html +++ /dev/null @@ -1,651 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/genex/genex.nocache.js b/common/static/js/capa/genex/genex.nocache.js index f457e80b6c..11f9714afb 100644 --- a/common/static/js/capa/genex/genex.nocache.js +++ b/common/static/js/capa/genex/genex.nocache.js @@ -1,4 +1,4 @@ -function genex(){var P='',xb='" for "gwt:onLoadErrorFn"',vb='" for "gwt:onPropertyErrorFn"',ib='"><\/script>',Z='#',Xb='.cache.html',_='/',lb='//',Qb='46DBCB09BEC38A6DEE76494C6517111B',Rb='557C7018CDCA52B163256408948A1722',Sb='866AF633CAA7EA4DA7E906456CDEC65A',Tb='8F9C3F1A91187AA8391FD08BA7F8716D',Wb=':',pb='::',dc=' + % endif +% endif diff --git a/lms/templates/courseware/gradebook.html b/lms/templates/courseware/gradebook.html index fb750aed19..015004ee1c 100644 --- a/lms/templates/courseware/gradebook.html +++ b/lms/templates/courseware/gradebook.html @@ -13,9 +13,12 @@ <%static:css group='course'/> @@ -78,8 +81,8 @@ letter_grade = 'None' if fraction > 0: letter_grade = 'F' - for grade in ['A', 'B', 'C']: - if fraction >= course.grade_cutoffs[grade]: + for (grade, cutoff) in ordered_grades: + if fraction >= cutoff: letter_grade = grade break @@ -90,11 +93,11 @@ %for student in students: - + %for section in student['grade_summary']['section_breakdown']: ${percent_data( section['percent'] )} %endfor - ${percent_data( student['grade_summary']['percent'])} + ${percent_data( student['grade_summary']['percent'])} %endfor diff --git a/lms/templates/discussion/_single_thread.html b/lms/templates/discussion/_single_thread.html index 0dec32ad47..e291bc955c 100644 --- a/lms/templates/discussion/_single_thread.html +++ b/lms/templates/discussion/_single_thread.html @@ -6,7 +6,7 @@
    - %if thread['group_id'] + %if thread['group_id']:
    This post visible only to group ${cohort_dictionary[thread['group_id']]}.
    %endif @@ -35,4 +35,4 @@ -<%include file="_js_data.html" /> \ No newline at end of file +<%include file="_js_data.html" /> diff --git a/lms/templates/discussion/mustache/_inline_thread_cohorted.mustache b/lms/templates/discussion/mustache/_inline_thread_cohorted.mustache index 9223dfd388..40921aeb4e 100644 --- a/lms/templates/discussion/mustache/_inline_thread_cohorted.mustache +++ b/lms/templates/discussion/mustache/_inline_thread_cohorted.mustache @@ -1,5 +1,6 @@
    {{group_string}}
    +
      diff --git a/lms/templates/discussion/mustache/_inline_thread_show.mustache b/lms/templates/discussion/mustache/_inline_thread_show.mustache index 733fbc4ca1..b961aadcc4 100644 --- a/lms/templates/discussion/mustache/_inline_thread_show.mustache +++ b/lms/templates/discussion/mustache/_inline_thread_show.mustache @@ -3,6 +3,9 @@
      + {{votes.up_count}}

      {{title}}

      +
      + Pinned
      +

      {{#user}} {{username}} diff --git a/lms/templates/feed.rss b/lms/templates/feed.rss index bd97113e33..07335bd22a 100644 --- a/lms/templates/feed.rss +++ b/lms/templates/feed.rss @@ -11,16 +11,16 @@ tag:www.edx.org,2012:Post/17 2012-12-19T14:00:00-07:00 2012-12-19T14:00:00-07:00 - + Stanford University to Collaborate with edX on Development of Non-Profit Open Source edX Platform - <img src="${static.url('images/press/releases/stanford-university_204x114.png')}" /> + <img src="${static.url('images/press/releases/stanford-university-m.png')}" /> <p></p> tag:www.edx.org,2013:Post/16 2013-03-15T10:00:00-07:00 2013-03-15T10:00:00-07:00 - + edX releases XBlock SDK, first step toward open source vision <img src="${static.url('images/press/releases/edx-logo_240x180.png')}" /> <p></p> @@ -38,7 +38,7 @@ - + @@ -47,7 +47,7 @@ tag:www.edx.org,2013:Post/14 2013-01-30T10:00:00-07:00 2013-01-30T10:00:00-07:00 - + New biology course from human genome pioneer Eric Lander <img src="${static.url('images/press/releases/eric-lander_240x180.jpg')}" /> <p></p> @@ -56,7 +56,7 @@ tag:www.edx.org,2013:Post/12 2013-01-22T10:00:00-07:00 2013-01-22T10:00:00-07:00 - + New course from legendary MIT physics professor Walter Lewin <img src="${static.url('images/press/releases/dr-lewin-316_240x180.jpg')}" /> <p></p> @@ -65,7 +65,7 @@ tag:www.edx.org,2013:Post/11 2013-01-29T10:00:00-07:00 2013-01-29T10:00:00-07:00 - + City of Boston and edX partner to establish BostonX to improve educational access for residents <img src="${static.url('images/press/releases/edx-logo_240x180.png')}" /> <p></p> @@ -74,7 +74,7 @@ - + @@ -83,7 +83,7 @@ tag:www.edx.org,2012:Post/9 2012-12-10T14:00:00-07:00 2012-12-10T14:00:00-07:00 - + Georgetown University joins edX <img src="${static.url('images/press/releases/georgetown-seal_240x180.png')}" /> <p>Sixth institution to join global movement in year one</p> @@ -92,7 +92,7 @@ tag:www.edx.org,2012:Post/8 2012-12-04T14:00:00-07:00 2012-12-04T14:00:00-07:00 - + Wellesley College joins edX <img src="${static.url('images/press/releases/wellesley-seal_240x180.png')}" /> <p>First liberal arts college to join edX</p> @@ -101,7 +101,7 @@ tag:www.edx.org,2012:Post/7 2012-11-12T14:00:00-07:00 2012-11-12T14:00:00-07:00 - + edX and Massachusetts Community Colleges join in Gates-Funded educational initiative <img src="${static.url('images/press/releases/mass-seal_240x180.png')}" /> <p></p> @@ -110,7 +110,7 @@ tag:www.edx.org,2012:Post/6 2012-10-15T14:00:00-07:00 2012-10-14T14:00:00-07:00 - + The University of Texas System joins edX <img src="${static.url('images/press/releases/utsys-seal_240x180.png')}" /> <p>Nine universities and six health institutions</p> @@ -119,7 +119,7 @@ - + @@ -128,7 +128,7 @@ tag:www.edx.org,2012:Post/4 2012-09-06T14:00:00-07:00 2012-09-06T14:00:00-07:00 - + edX to offer learners option of taking proctored final exam <img src="${static.url('images/press/releases/diploma_240x180.jpg')}" /> @@ -136,7 +136,7 @@ tag:www.edx.org,2012:Post/3 2012-07-16T14:08:12-07:00 2012-07-16T14:08:12-07:00 - + UC Berkeley joins edX <img src="${static.url('images/press/releases/edx-logo_240x180.png')}" /> <p>edX broadens course offerings</p> diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index 87559ec877..5ad3136815 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -43,8 +43,8 @@

      Please include some written feedback as well.

      -
      Flag this submission for review by course staff (use if the submission contains inappropriate content)
      -
      I do not know how to grade this question
      +
      This submission has explicit or pornographic content :
      +
      I do not know how to grade this question :
    @@ -82,6 +82,19 @@ - + +
    +

    Are you sure that you want to flag this submission?

    +

    + You are about to flag a submission. You should only flag a submission that contains explicit or offensive content. If the submission is not addressed to the question or is incorrect, you should give it a score of zero and accompanying feedback instead of flagging it. +

    +
    + + +
    +
    + + + diff --git a/lms/templates/static_htmlbook.html b/lms/templates/static_htmlbook.html index 9500a379ac..830ddddca9 100644 --- a/lms/templates/static_htmlbook.html +++ b/lms/templates/static_htmlbook.html @@ -26,32 +26,22 @@ // chapters, and it should be in-bounds. chapterToLoad = options.chapterNum; } - var anchorToLoad = null; - if (options.chapters) { - anchorToLoad = options.anchor_id; - } - loadUrl = function htmlViewLoadUrl(url, anchorId) { + loadUrl = function htmlViewLoadUrl(url) { // clear out previous load, if any: parentElement = document.getElementById('bookpage'); while (parentElement.hasChildNodes()) parentElement.removeChild(parentElement.lastChild); // load new URL in: $('#bookpage').load(url); + }; - // if there is an anchor set, then go to that location: - if (anchorId != null) { - // TODO: add implementation.... - } - - }; - - loadChapterUrl = function htmlViewLoadChapterUrl(chapterNum, anchorId) { + loadChapterUrl = function htmlViewLoadChapterUrl(chapterNum) { if (chapterNum < 1 || chapterNum > chapterUrls.length) { return; } var chapterUrl = chapterUrls[chapterNum-1]; - loadUrl(chapterUrl, anchorId); + loadUrl(chapterUrl); }; // define navigation links for chapters: @@ -64,15 +54,15 @@ }; for (var index = 1; index <= chapterUrls.length; index += 1) { $("#htmlchapter-" + index).click(loadChapterUrlHelper(index)); - } + } } // finally, load the appropriate url/page if (urlToLoad != null) { - loadUrl(urlToLoad, anchorToLoad); + loadUrl(urlToLoad); } else { - loadChapterUrl(chapterToLoad, anchorToLoad); - } + loadChapterUrl(chapterToLoad); + } } })(jQuery); @@ -92,9 +82,6 @@ %if chapter is not None: options.chapterNum = ${chapter}; %endif - %if anchor_id is not None: - options.anchor_id = ${anchor_id}; - %endif $('#outerContainer').myHTMLViewer(options); }); diff --git a/lms/templates/static_templates/contact.html b/lms/templates/static_templates/contact.html index d848164720..79e2743dbc 100644 --- a/lms/templates/static_templates/contact.html +++ b/lms/templates/static_templates/contact.html @@ -33,6 +33,9 @@

    Universities

    If you are a university wishing to collaborate or with questions about edX, please email university@edx.org.

    +

    Accessibility

    +

    EdX strives to create an innovative online-learning platform that promotes accessibility for everyone, including students with disabilities. We are dedicated to improving the accessibility of the platform and welcome your comments or questions at accessibility@edx.org.

    + diff --git a/lms/templates/static_templates/jobs.html b/lms/templates/static_templates/jobs.html index 7d57abb47e..18ef1119e1 100644 --- a/lms/templates/static_templates/jobs.html +++ b/lms/templates/static_templates/jobs.html @@ -75,19 +75,19 @@
    -

    VICE PRESIDENT/DIRECTOR OF EDUCATIONAL SERVICES

    -

    The edX VP/Director of Education Services reporting to the VP of Engineering and Educational Services is responsible for:

    +

    DIRECTOR OF EDUCATION SERVICES

    +

    The edX Director of Education Services reporting to the VP of Engineering and Education Services is responsible for:

    1. Delivering 20 new courses in 2013 in collaboration with the partner Universities
        -
      • Reporting to the Director of Educational Services are the Video production team, responsible for post-production of Course Video. The Director must understand how to balance artistic quality and learning objectives, and reduce production time so that video capabilities are readily accessible and at reasonable costs.

        -
      • Reporting to the Director are a small team of Program Managers, who are responsible for managing the day to day of course production and operations. The Director must be experienced in capacity planning and operations, understand how to deploy lean collaboration and able to build alliances inside edX and the University. In conjunction with the Program Managers, the Director of Educational Services will supervise the collection of research, the retrospectives with Professors and the assembly of best practices in course production and operations. The three key deliverables are the use of a well-defined lean process for onboarding Professors, the development of tracking tools, and assessment of effectiveness of Best Practices. -
      • Also reporting to the Director of Education Services are content engineers and Course Fellows, skilled in the development of edX assessments. The Director of Educational Services will also be responsible for communicating to the VP of Engineering requirements for new types of course assessments. Course Fellows are extremely talented Ph.D.’s who work directly with the Professors to define and develop assessments and course curriculum.
      • +
      • Reporting to the Director of Education Services are the Video production team, responsible for post-production of Course Video. The Director must understand how to balance artistic quality and learning objectives, and reduce production time so that video capabilities are readily accessible and at reasonable costs.

        +
      • Reporting to the Director are a small team of Program Managers, who are responsible for managing the day to day of course production and operations. The Director must be experienced in capacity planning and operations, understand how to deploy lean collaboration and able to build alliances inside edX and the University. In conjunction with the Program Managers, the Director of Education Services will supervise the collection of research, the retrospectives with Professors and the assembly of best practices in course production and operations. The three key deliverables are the use of a well-defined lean process for onboarding Professors, the development of tracking tools, and assessment of effectiveness of Best Practices. +
      • Also reporting to the Director of Education Services are content engineers and Course Fellows, skilled in the development of edX assessments. The Director of Education Services will also be responsible for communicating to the VP of Engineering requirements for new types of course assessments. Course Fellows are extremely talented Ph.D.’s who work directly with the Professors to define and develop assessments and course curriculum.
    2. Training and Onboarding of 30 Partner Universities and Affiliates
        -
      • The edX Director of Educational Services is responsible for building out the Training capabilities and delivery mechanisms for onboarding Professors at partner Universities. The edX Director must build out both the Training Team and the curriculum. Training will be delivered in both online courses, self-paced formats, and workshops. The training must cover a curriculum that enables partner institutions to be completely independent. Additionally, partner institutions should be engaged to contribute to the curriculum and partner with edX in the delivery of the material. The curriculum must exemplify the best in online learning, so the Universities are inspired to offer the kind of learning they have experienced in their edX Training.
      • +
      • The edX Director of Education Services is responsible for building out the Training capabilities and delivery mechanisms for onboarding Professors at partner Universities. The edX Director must build out both the Training Team and the curriculum. Training will be delivered in both online courses, self-paced formats, and workshops. The training must cover a curriculum that enables partner institutions to be completely independent. Additionally, partner institutions should be engaged to contribute to the curriculum and partner with edX in the delivery of the material. The curriculum must exemplify the best in online learning, so the Universities are inspired to offer the kind of learning they have experienced in their edX Training.
      • Expand and extend the education goals of the partner Universities by operationalizing best practices.
      • Engage with University Boards to design and define the success that the technology makes possible.
      @@ -117,7 +117,7 @@
    3. Develop team skills in a ferociously intelligent group
    4. Fan the enthusiasm of the partner Universities when the enormity of the transition they are facing becomes intimidating
    5. Encourage creativity to allow the technology to provoke pedagogical possibilities that brick and mortar classes have precluded.
    6. -
    7. Lean and Agile thinking and training. Experienced in scrum or kanban.
    8. +
    9. Lean and Agile thinking and training. Experienced in Scrum or Kanban.
    10. Design and deliver hiring/development plans which meet rapidly changing skill needs.
    11. @@ -128,10 +128,10 @@

      MANAGER OF TRAINING SERVICES

      -

      The Manager of Training Services is an integral member of the edX team, a leader who is also a doer, working hands-on in the development and delivery of edX’s training portfolio. Reporting to the Director of Educational Services, the manager will be a strategic thinker, providing leadership and vision in the development of world-class training solutions tailored to meet the diverse needs of edX Universities, partners and stakeholders

      +

      The Manager of Training Services is an integral member of the edX team, a leader who is also a doer, working hands-on in the development and delivery of edX’s training portfolio. Reporting to the Director of Education Services, the manager will be a strategic thinker, providing leadership and vision in the development of world-class training solutions tailored to meet the diverse needs of edX Universities, partners and stakeholders

      Responsibilities:

        -
      • Working with the Director of Educational Services, create and manage a world-class training program that includes in-person workshops and online formats such as self-paced courses, and webinars.
      • +
      • Working with the Director of Education Services, create and manage a world-class training program that includes in-person workshops and online formats such as self-paced courses, and webinars.
      • Work across a talented team of product developers, video producers and content experts to identify training needs and proactively develop training curricula for new products and services as they are deployed.
      • Develop the means for sharing and showcasing edX best practices for both internal and external audiences.
      • Apply sound instructional design theory and practice in the development of all edX training resources.
      • @@ -150,7 +150,7 @@

        Requirements:

        • Minimum of 5-7 years experience developing and delivering educational training, preferably in an educational technology organization.
        • -
        • Lean and Agile thinking and training. Experienced in Scrum or kanban.
        • +
        • Lean and Agile thinking and training. Experienced in Scrum or Kanban.
        • Excellent interpersonal skills including proven presentation and facilitation skills.
        • Strong oral and written communication skills.
        • Proven experience with production and delivery of online training programs that utilize asychronous and synchronous delivery mechanisms.
        • @@ -187,7 +187,7 @@

          Requirements:

          • Minimum of 1-3 years experience developing and delivering educational training, preferably in an educational technology organization.
          • -
          • Lean and Agile thinking and training. Experienced in Scrum or kanban preferred.
          • +
          • Lean and Agile thinking and training. Experienced in Scrum or Kanban preferred.
          • Excellent interpersonal skills including proven presentation and facilitation skills.
          • Strong oral and written communication skills.
          • Flexibility to work on a variety of initiatives; prior startup experience preferred.
          • @@ -229,7 +229,7 @@
          • Excellent interpersonal and communication (written and verbal), project management, problem-solving and time management skills. The ability to be flexible with projects and to work on multiple courses essential.
          • Ability to meet deadlines and manage expectations of constituents.
          • Capacity to develop new and relevant technology skills. Experience using game theory design and learning analytics to inform instructional design decisions and strategy.
          • -
          • Technical Skills: Video and screencasting experience. LMS Platform experience, xml, HTML, CSS, Adobe Design Suite, Camtasia or Captivate experience. Experience with web 2.0 collaboration tools.
          • +
          • Technical Skills: Video and screencasting experience. LMS Platform experience, XML, HTML, CSS, Adobe Design Suite, Camtasia or Captivate experience. Experience with web 2.0 collaboration tools.

          Eligible candidates will be invited to respond to an Instructional Design task based on current or future edX course development needs.

          @@ -241,7 +241,7 @@

          PROGRAM MANAGER

          -

          edX Program Managers (PM) lead the edX's course production process. They are systems thinkers who manage the creation of a course from start to finish. PMs work with University Professors and course staff to help them take advantage of edX services to create world class online learning offerings and encourage the exploration of an emerging form of higher education.

          +

          EdX Program Managers (PM) lead the edX's course production process. They are systems thinkers who manage the creation of a course from start to finish. PMs work with University Professors and course staff to help them take advantage of edX services to create world class online learning offerings and encourage the exploration of an emerging form of higher education.

          Responsibilities:

          • Create and execute the course production cycle. PMs are able to examine and explain what they do in great detail and able to think abstractly about people, time, and processes. They coordinate the efforts of multiple teams engaged in the production of the courses assigned to them.
          • @@ -317,8 +317,8 @@

            Content engineers help create the technology for specific courses. The tasks include:

            • Developing of course-specific user-facing elements, such as the circuit editor and simulator.
            • -
            • Integrating course materials into courses
            • -
            • Creating programs to grade questions designed with complex technical features
            • +
            • Integrating course materials into courses.
            • +
            • Creating programs to grade questions designed with complex technical features.
            • Knowledge of Python, XML, and/or JavaScript is desired. Strong interest and background in pedagogy and education is desired as well.
            • Building course components in straight XML or through our course authoring tool, edX Studio.
            • Assisting University teams and in house staff take advantage of new course software, including designing and developing technical refinements for implementation.
            • @@ -328,13 +328,13 @@

              Qualifications:

              • Bachelor’s degree or higher. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
              • -
              • Thorough knowledge of Python, DJango, XML,HTML, CSS , Javascript and backbone.js
              • -
              • Ability to work on multiple projects simultaneously without splintering
              • +
              • Thorough knowledge of Python, DJango, XML, HTML, CSS, JavaScript and backbone.js.
              • +
              • Ability to work on multiple projects simultaneously without splintering.
              • Tactfully escalate conflicting deadlines or priorities only when needed. Otherwise help the team members negotiate a solution.
              • Unfailing attention to detail, especially the details the course teams have seen so often they don’t notice them anymore.
              • -
              • Readily zoom from the big picture to the smallest course component to notice when typos, inconsistencies or repetitions have unknowingly crept in
              • +
              • Readily zoom from the big picture to the smallest course component to notice when typos, inconsistencies or repetitions have unknowingly crept in.
              • Curiosity to step into the shoes of an online student working to master the course content.
              • -
              • Solid interpersonal skills, especially good listening
              • +
              • Solid interpersonal skills, especially good listening.

              If you are interested in this position, please send an email to jobs@edx.org.

              @@ -345,7 +345,7 @@

              SOFTWARE ENGINEER

              -

              edX is looking for engineers who can contribute to its Open Source learning platform. We are a small team with a startup, lean culture, committed to building open-source software that scales and dramatically changes the face of education. Our ideal candidates are hands on developers who understand how to build scalable, service based systems, preferably in Python and have a proven track record of bringing their ideas to market. We are looking for engineers with all levels of experience, but you must be a proven leader and outstanding developer to work at edX.

              +

              EdX is looking for engineers who can contribute to its Open Source learning platform. We are a small team with a startup, lean culture, committed to building open-source software that scales and dramatically changes the face of education. Our ideal candidates are hands on developers who understand how to build scalable, service based systems, preferably in Python and have a proven track record of bringing their ideas to market. We are looking for engineers with all levels of experience, but you must be a proven leader and outstanding developer to work at edX.

              There are a number of projects for which we are recruiting engineers:
              @@ -360,14 +360,14 @@

              Requirements:

              • Real-world experience with Python or other dynamic development languages.
              • -
              • Able to code front to back, including HTML, CSS, Javascript, Django, Python
              • -
              • You must be committed to an agile development practices, in Scrum or Kanban
              • -
              • Demonstrated skills in building Service based architecture
              • -
              • Test Driven Development
              • -
              • Committed to Documentation best practices so your code can be consumed in an open source environment
              • -
              • Contributor to or consumer of Open Source Frameworks
              • +
              • Able to code front to back, including HTML, CSS, JavaScript, Django, Python.
              • +
              • You must be committed to an agile development practices, in Scrum or Kanban.
              • +
              • Demonstrated skills in building Service based architecture.
              • +
              • Test Driven Development.
              • +
              • Committed to Documentation best practices so your code can be consumed in an open source environment.
              • +
              • Contributor to or consumer of Open Source Frameworks.
              • BS in Computer Science from top-tier institution. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
              • -
              • Acknowledged by peers as a technology leader
              • +
              • Acknowledged by peers as a technology leader.

              If you are interested in this position, please send an email to jobs@edx.org.

              @@ -377,7 +377,7 @@
              -

              DEVOPS ENGINEER – SYESTEMS ADMINISTRATOR

              +

              DEVOPS ENGINEER – SYSTEMS ADMINISTRATOR

              The Devop Engineers at edX help develop and maintain the infrastructure in AWS for all services and systems required to run edX. We're seeking a capable systems administrator who is unafraid of scripting languages and development to build out tools in order to improve the functionality of edX. The devops team primarily focuses on the provisioning, configuration, and deployment of services at edX. If you have a passion for automation and constant improvement then we want to hear from you. Our production environment is primarily built on Ubuntu (in AWS) and we use Puppet and Fabric to manage most of the environment.

              In addition to the primary task of building infrastructure the Devops team supports the developers in a variety of other contexts, including helping with desktop development environments if required. We participate in on-call and emergency support and there will be occasional out of normal hours work required.

              Responsibilities:

              @@ -388,7 +388,8 @@

            Requirements:

              -
            • Bachelor's degree in engineering or computer science. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.3 or more years of systems administration.
            • +
            • Bachelor's degree in engineering or computer science. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
            • +
            • Three or more years of systems administration.
            • Must have an excellent working knowledge of Linux both as an end-user and as an administrator.
            • Must be adept in programming/scripting languages such as Python, Ruby, Bash.
            • Must be familiar with a configuration management system such as Puppet, Chef, Ansible.
            • @@ -475,7 +476,7 @@ development and program management teams.

            • Proactive, optimistic approach to problem solving.
            • Commitment to constant personal and organizational improvement.
            • Willingness to travel to partner sites as needed.
            • -
            • Lean and Agile thinking and training. Experienced in Scrum or kanban.
            • +
            • Lean and Agile thinking and training. Experienced in Scrum or Kanban.
            • Bachelors or Master’s in Education, organizational learning, or other related field preferred. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
            @@ -483,22 +484,86 @@ development and program management teams.

          +
          +
          +

          WEB DESIGNER, PRODUCT TEAM

          + +

          EdX is looking for a Web Designer to join our Product Team and shape the experience of edX's online learning tools. With thousands and thousands of students and hundreds of professors using our software every day, our online learning tools have to sing. Our ideal candidates are passionate and picky about what makes a good user experience; sweat the mechanical, visual, and transactional details when designing; know how to bring an idea or project from a sketch on paper to being alive in a browser; can instinctually bring organization to a design meeting, deliverable, or project; and thrive on collaboration with colleagues and constant iteration/refinement.

          + +

          As an edX Designer, you:

          +
            +
          • Have an innate sense of – and strong opinion about – good usability when it comes to web applications, and an ability to clearly articulate both.
          • +
          • Understand established interactive technologies and possess an undying thirst to learn about new ones.
          • +
          • Define and work within visual themes based on your excellent understanding of grids, typography, color, and design principles.
          • +
          • Marry design aesthetics to user experiences while keeping in mind accessibility, usability, and web standards.
          • +
          • Can use HTML5, CSS3, and DOM-manipulating JavaScript to represent your designs in the browser.
          • +
          • Conceptualize and articulate complex ideas to drive decisions, facilitate understanding, and reach consensus.
          • +
          • Document your thinking using appropriately chosen, informed deliverables such as sketches, wireframes, prototypes, site maps/flows, personas, style tiles, and design comps.
          • +
          • Have a perfectionist mindset, but won’t lose momentum in projects because of it.
          • +
          • Expertly present user experience and design recommendations to team members.
          • +
          + +

          Requirements:

          +
            +
          • Have at least 2 years of professional, post-collegiate experience.
          • +
          • Have a BA, BS, BFA, or equivalent work experience in areas such as human-computer interaction, information science, graphic or industrial design, computer science, fine arts, social sciences such as psychology, or another related field. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
          • +
          +

          About the Product Design Team:

          +

          We are a small team with a startup, lean culture, committed to building tools that help our users learn and teach online. Working alongside developers, course staff, product owners, and project stakeholders, our Designers shepherd the experience of an idea or tool through research and strategy phases and lead the Information Architecture, Interaction Design, Visual Design, and Front End Development efforts in bringing that experience to life. We enjoy holding Design Studio exercises, finding the right design tool to do the job efficiently, and our CSS preprocessors.

          + +

          If you wish to apply, please send your resume (PDF, text, or Word Doc), a thoughtful email that includes specifics about how your previous experience matches the Designer role at edX, and online samples of your work to jobs@edx.org. Candidates who do not provide these will not be considered. EdX is open to considering candidates outside of the Boston/Cambridge, MA area who are willing to relocate.

          +
          +
          + +
          +
          +

          FRONT END DEVELOPER

          +

          edX is looking for a Front End Developer to join our Product and Engineering Teams to shape the experience of all of edX's online learning tools. Thousands of students learn with us every day – the way they connect with their courses, their professors and edX is through our ever more powerful front end. Our ideal candidates not only know modern front end development best practices, but make organization standards and teach others with them; sweat the mechanical, visual, and transactional details when bring a design to life in the browser; can instinctually bring organization to their HTML/CSS/JavaScript, documentation, or project; and thrive on collaborating with both designers and developers throughout a project's lifecycle.

          +

          As an edX Front End Developer, you:

          +
            +
          • Translate flat design comps, wireframes, and prototypes to production-ready interactive interfaces with joy and passion.
          • +
          • Are very familiar with cutting-edge front-end development practices and technology (CSS3, media queries, responsive web design, HTML5, etc.).
          • +
          • Write JavaScript without the use of a library while still being familiar with popular libraries such as jQuery.
          • +
          • Can abstract layouts, design patterns, and UI components while building out the interface to a product or application.
          • +
          • Appreciate that web standards, accessibility, and usability are essential to uphold.
          • +
          • Generally have experience with server-side templating and data extraction code while enjoying learning more from the development team.
          • +
          • Maintain the sanctity of a project's information architecture, interaction design, and visual design details while contributing to the effort.
          • +
          • Know how to test and refactor your code across browsers and with QA teams.
          • +
          • Work well with designers, developers, and colleagues.
          • +
          • Take pride in your communicative and collaborative abilities.
          • +
          + +

          Front End Developers must also:

          +
            +
          • Have at least two years of professional, post-collegiate experience.
          • +
          • Have a BS, BFA or equivalent work experience. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
          • +
          + +

          About the Product Design and Development Teams:

          +

          We are a small team with a startup, lean culture, committed to building tools that help our users learn and teach online. Working alongside developers, course staff, product owners, and project stakeholders, our Designers shepherd the experience of an idea or tool through research and strategy phases and lead the Information Architecture, Interaction Design, Visual Design, and Front End Development efforts in bringing that experience to life.

          + +

          If you wish to apply, please send your resume (PDF, text, or Word Doc), a thoughtful email that includes specifics about how your previous experience matches the Front End Developer role at edX, and online samples of your work to jobs@edx.org. Candidates who do not provide these will not be considered. EdX is open to considering candidates outside of the Boston/Cambridge, MA area who are willing to relocate.

          +
          +
          +

          Positions

          How to Apply

          E-mail your resume, cover letter and any other materials to jobs@edx.org

          diff --git a/lms/templates/static_templates/press_releases/Cengage_to_provide_book_content.html b/lms/templates/static_templates/press_releases/cengage_to_provide_book_content.html similarity index 100% rename from lms/templates/static_templates/press_releases/Cengage_to_provide_book_content.html rename to lms/templates/static_templates/press_releases/cengage_to_provide_book_content.html diff --git a/lms/templates/static_templates/press_releases/edX_announces_proctored_exam_testing.html b/lms/templates/static_templates/press_releases/edx_announces_proctored_exam_testing.html similarity index 100% rename from lms/templates/static_templates/press_releases/edX_announces_proctored_exam_testing.html rename to lms/templates/static_templates/press_releases/edx_announces_proctored_exam_testing.html diff --git a/lms/templates/static_templates/press_releases/Elsevier_collaborates_with_edX.html b/lms/templates/static_templates/press_releases/elsevier_collaborates_with_edx.html similarity index 100% rename from lms/templates/static_templates/press_releases/Elsevier_collaborates_with_edX.html rename to lms/templates/static_templates/press_releases/elsevier_collaborates_with_edx.html diff --git a/lms/templates/static_templates/press_releases/Gates_Foundation_announcement.html b/lms/templates/static_templates/press_releases/gates_foundation_announcement.html similarity index 100% rename from lms/templates/static_templates/press_releases/Gates_Foundation_announcement.html rename to lms/templates/static_templates/press_releases/gates_foundation_announcement.html diff --git a/lms/templates/static_templates/press_releases/Georgetown_joins_edX.html b/lms/templates/static_templates/press_releases/georgetown_joins_edx.html similarity index 100% rename from lms/templates/static_templates/press_releases/Georgetown_joins_edX.html rename to lms/templates/static_templates/press_releases/georgetown_joins_edx.html diff --git a/lms/templates/static_templates/press_releases/Lewin_course_announcement.html b/lms/templates/static_templates/press_releases/lewin_course_announcement.html similarity index 100% rename from lms/templates/static_templates/press_releases/Lewin_course_announcement.html rename to lms/templates/static_templates/press_releases/lewin_course_announcement.html diff --git a/lms/templates/static_templates/press_releases/MIT_and_Harvard_announce_edX.html b/lms/templates/static_templates/press_releases/mit_and_harvard_announce_edx.html similarity index 100% rename from lms/templates/static_templates/press_releases/MIT_and_Harvard_announce_edX.html rename to lms/templates/static_templates/press_releases/mit_and_harvard_announce_edx.html diff --git a/lms/templates/static_templates/press_releases/Spring_2013_course_announcements.html b/lms/templates/static_templates/press_releases/spring_courses.html similarity index 100% rename from lms/templates/static_templates/press_releases/Spring_2013_course_announcements.html rename to lms/templates/static_templates/press_releases/spring_courses.html diff --git a/lms/templates/static_templates/press_releases/stanford_announcement.html b/lms/templates/static_templates/press_releases/stanford_to_work_with_edx.html similarity index 100% rename from lms/templates/static_templates/press_releases/stanford_announcement.html rename to lms/templates/static_templates/press_releases/stanford_to_work_with_edx.html diff --git a/lms/templates/static_templates/press_releases/UC_Berkeley_joins_edX.html b/lms/templates/static_templates/press_releases/uc_berkeley_joins_edx.html similarity index 100% rename from lms/templates/static_templates/press_releases/UC_Berkeley_joins_edX.html rename to lms/templates/static_templates/press_releases/uc_berkeley_joins_edx.html diff --git a/lms/templates/static_templates/press_releases/UT_joins_edX.html b/lms/templates/static_templates/press_releases/ut_joins_edx.html similarity index 100% rename from lms/templates/static_templates/press_releases/UT_joins_edX.html rename to lms/templates/static_templates/press_releases/ut_joins_edx.html diff --git a/lms/templates/static_templates/press_releases/Wellesley_College_joins_edX.html b/lms/templates/static_templates/press_releases/wellesley_college_joins_edx.html similarity index 100% rename from lms/templates/static_templates/press_releases/Wellesley_College_joins_edX.html rename to lms/templates/static_templates/press_releases/wellesley_college_joins_edx.html diff --git a/lms/urls.py b/lms/urls.py index 3bcfc25d3d..49dae19d58 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -34,12 +34,6 @@ urlpatterns = ('', url(r'^accept_name_change$', 'student.views.accept_name_change'), url(r'^reject_name_change$', 'student.views.reject_name_change'), url(r'^pending_name_changes$', 'student.views.pending_name_changes'), - - url(r'^testcenter/login$', 'student.views.test_center_login'), - - # url(r'^testcenter/login$', 'student.test_center_views.login'), - # url(r'^testcenter/logout$', 'student.test_center_views.logout'), - url(r'^event$', 'track.views.user_track'), url(r'^t/(?P