Commit f1a899ac by ichuang

Merge branch 'master' of github.com:MITx/mitx into feature/ichuang/masquerade-v3

parents 81621fbe 4615f0d8
...@@ -34,10 +34,13 @@ load-plugins= ...@@ -34,10 +34,13 @@ load-plugins=
# multiple time (only on the command line, not in the configuration file where # multiple time (only on the command line, not in the configuration file where
# it should appear only once). # it should appear only once).
disable= disable=
# Never going to use these
# C0301: Line too long # C0301: Line too long
# C0302: Too many lines in module
# W0141: Used builtin function 'map'
# W0142: Used * or ** magic # 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 # R0201: Method could be a function
# R0901: Too many ancestors # R0901: Too many ancestors
# R0902: Too many instance attributes # R0902: Too many instance attributes
...@@ -96,7 +99,18 @@ zope=no ...@@ -96,7 +99,18 @@ zope=no
# List of members which are set dynamically and missed by pylint inference # List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed. Python regular # system, and so shouldn't trigger E0201 when accessed. Python regular
# expressions are accepted. # 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] [BASIC]
......
import logging
import sys
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
...@@ -131,7 +128,7 @@ def remove_user_from_course_group(caller, user, location, role): ...@@ -131,7 +128,7 @@ def remove_user_from_course_group(caller, user, location, role):
raise PermissionDenied raise PermissionDenied
# see if the user is actually in that role, if not then we don't have to do anything # 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) groupname = get_course_groupname_for_role(location, role)
group = Group.objects.get(name=groupname) group = Group.objects.get(name=groupname)
......
...@@ -11,6 +11,7 @@ Feature: Advanced (manual) course policy ...@@ -11,6 +11,7 @@ Feature: Advanced (manual) course policy
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
Then the settings are alphabetized Then the settings are alphabetized
@skip-phantom
Scenario: Test cancel editing key value Scenario: Test cancel editing key value
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key When I edit the value of a policy key
...@@ -19,6 +20,7 @@ Feature: Advanced (manual) course policy ...@@ -19,6 +20,7 @@ Feature: Advanced (manual) course policy
And I reload the page And I reload the page
Then the policy key value is unchanged Then the policy key value is unchanged
@skip-phantom
Scenario: Test editing key value Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key and save When I edit the value of a policy key and save
...@@ -26,6 +28,7 @@ Feature: Advanced (manual) course policy ...@@ -26,6 +28,7 @@ Feature: Advanced (manual) course policy
And I reload the page And I reload the page
Then the policy key value is changed Then the policy key value is changed
@skip-phantom
Scenario: Test how multi-line input appears Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I create a JSON object as a value When I create a JSON object as a value
...@@ -33,6 +36,7 @@ Feature: Advanced (manual) course policy ...@@ -33,6 +36,7 @@ Feature: Advanced (manual) course policy
And I reload the page And I reload the page
Then it is displayed as formatted Then it is displayed as formatted
@skip-phantom
Scenario: Test automatic quoting of non-JSON values Scenario: Test automatic quoting of non-JSON values
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I create a non-JSON value not in quotes When I create a non-JSON value not in quotes
......
...@@ -3,10 +3,7 @@ ...@@ -3,10 +3,7 @@
from lettuce import world, step from lettuce import world, step
from common import * from common import *
import time from nose.tools import assert_false, assert_equal
from terrain.steps import reload_the_page
from nose.tools import assert_true, assert_false, assert_equal
""" """
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html
...@@ -18,8 +15,8 @@ VALUE_CSS = 'textarea.json' ...@@ -18,8 +15,8 @@ VALUE_CSS = 'textarea.json'
DISPLAY_NAME_KEY = "display_name" DISPLAY_NAME_KEY = "display_name"
DISPLAY_NAME_VALUE = '"Robot Super Course"' DISPLAY_NAME_VALUE = '"Robot Super Course"'
############### ACTIONS ####################
############### ACTIONS ####################
@step('I select the Advanced Settings$') @step('I select the Advanced Settings$')
def i_select_advanced_settings(step): def i_select_advanced_settings(step):
expand_icon_css = 'li.nav-course-settings i.icon-expand' expand_icon_css = 'li.nav-course-settings i.icon-expand'
...@@ -38,7 +35,7 @@ def i_am_on_advanced_course_settings(step): ...@@ -38,7 +35,7 @@ def i_am_on_advanced_course_settings(step):
@step(u'I press the "([^"]*)" notification button$') @step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name): def press_the_notification_button(step, name):
css = 'a.%s-button' % name.lower() 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$') @step(u'I edit the value of a policy key$')
...@@ -52,7 +49,7 @@ def edit_the_value_of_a_policy_key(step): ...@@ -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$') @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"') change_display_name_value(step, '"foo"')
...@@ -90,7 +87,7 @@ def it_is_formatted(step): ...@@ -90,7 +87,7 @@ def it_is_formatted(step):
@step('it is displayed as a string') @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"']) assert_policy_entries([DISPLAY_NAME_KEY], ['"quote me"'])
......
...@@ -10,6 +10,8 @@ Feature: Course checklists ...@@ -10,6 +10,8 @@ Feature: Course checklists
Then I can check and uncheck tasks in a checklist Then I can check and uncheck tasks in a checklist
And They are correctly selected after I reload the page And They are correctly selected after I reload the page
@skip-phantom
@skip-firefox
Scenario: A task can link to a location within Studio Scenario: A task can link to a location within Studio
Given I have opened Checklists Given I have opened Checklists
When I select a link to the course outline When I select a link to the course outline
...@@ -17,8 +19,9 @@ Feature: Course checklists ...@@ -17,8 +19,9 @@ Feature: Course checklists
And I press the browser back button And I press the browser back button
Then I am brought back to the course outline in the correct state 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 Scenario: A task can link to a location outside Studio
Given I have opened Checklists Given I have opened Checklists
When I select a link to help page When I select a link to help page
Then I am brought to the help page in a new window Then I am brought to the help page in a new window
...@@ -6,6 +6,7 @@ from nose.tools import assert_true, assert_equal ...@@ -6,6 +6,7 @@ from nose.tools import assert_true, assert_equal
from terrain.steps import reload_the_page from terrain.steps import reload_the_page
from selenium.common.exceptions import StaleElementReferenceException from selenium.common.exceptions import StaleElementReferenceException
############### ACTIONS #################### ############### ACTIONS ####################
@step('I select Checklists from the Tools menu$') @step('I select Checklists from the Tools menu$')
def i_select_checklists(step): def i_select_checklists(step):
...@@ -88,8 +89,6 @@ def i_am_brought_to_help_page_in_new_window(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) assert_equal('http://help.edge.edx.org/', world.browser.url)
############### HELPER METHODS #################### ############### HELPER METHODS ####################
def verifyChecklist2Status(completed, total, percentage): def verifyChecklist2Status(completed, total, percentage):
def verify_count(driver): def verify_count(driver):
...@@ -106,9 +105,11 @@ def verifyChecklist2Status(completed, total, percentage): ...@@ -106,9 +105,11 @@ def verifyChecklist2Status(completed, total, percentage):
def toggleTask(checklist, task): 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): def clickActionLink(checklist, task, actionText):
# toggle checklist item to make sure that the link button is showing # toggle checklist item to make sure that the link button is showing
toggleTask(checklist, task) toggleTask(checklist, task)
...@@ -120,4 +121,3 @@ def clickActionLink(checklist, task, actionText): ...@@ -120,4 +121,3 @@ def clickActionLink(checklist, task, actionText):
world.wait_for(verify_action_link_text) world.wait_for(verify_action_link_text)
action_link.click() action_link.click()
Feature: Course Settings Feature: Course Settings
As a course author, I want to be able to configure my course settings. As a course author, I want to be able to configure my course settings.
@skip-phantom
Scenario: User can set course dates Scenario: User can set course dates
Given I have opened a new course in Studio Given I have opened a new course in Studio
When I select Schedule and Details When I select Schedule and Details
And I set course dates And I set course dates
Then I see the set dates on refresh Then I see the set dates on refresh
@skip-phantom
Scenario: User can clear previously set course dates (except start date) Scenario: User can clear previously set course dates (except start date)
Given I have set course dates Given I have set course dates
And I clear all the dates except start And I clear all the dates except start
Then I see cleared dates on refresh Then I see cleared dates on refresh
@skip-phantom
Scenario: User cannot clear the course start date Scenario: User cannot clear the course start date
Given I have set course dates Given I have set course dates
And I clear the course start date And I clear the course start date
......
...@@ -3,6 +3,7 @@ Feature: Create Section ...@@ -3,6 +3,7 @@ Feature: Create Section
As a course author As a course author
I want to create and edit sections I want to create and edit sections
@skip-phantom
Scenario: Add a new section to a course Scenario: Add a new section to a course
Given I have opened a new course in Studio Given I have opened a new course in Studio
When I click the New Section link When I click the New Section link
......
Feature: Overview Toggle Section Feature: Overview Toggle Section
In order to quickly view the details of a course's section or to scan the inventory of sections In order to quickly view the details of a course's section or to scan the inventory of sections
As a course author As a course author
I want to toggle the visibility of each section's subsection details in the overview listing 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 Scenario: The default layout for the overview page is to show sections in expanded view
Given I have a course with multiple sections Given I have a course with multiple sections
When I navigate to the course overview page When I navigate to the course overview page
Then I see the "Collapse All Sections" link Then I see the "Collapse All Sections" link
And all sections are expanded And all sections are expanded
Scenario: Expand /collapse for a course with no sections Scenario: Expand /collapse for a course with no sections
Given I have a course with no sections Given I have a course with no sections
When I navigate to the course overview page When I navigate to the course overview page
Then I do not see the "Collapse All Sections" link Then I do not see the "Collapse All Sections" link
@skip-phantom
Scenario: Collapse link appears after creating first section of a course Scenario: Collapse link appears after creating first section of a course
Given I have a course with no sections Given I have a course with no sections
When I navigate to the course overview page When I navigate to the course overview page
And I add a section And I add a section
Then I see the "Collapse All Sections" link Then I see the "Collapse All Sections" link
And all sections are expanded And all sections are expanded
@skip-phantom @skip-phantom
Scenario: Collapse link is not removed after last section of a course is deleted Scenario: Collapse link is not removed after last section of a course is deleted
Given I have a course with 1 section Given I have a course with 1 section
And I navigate to the course overview page And I navigate to the course overview page
When I press the "section" delete icon When I press the "section" delete icon
And I confirm the alert And I confirm the alert
Then I see the "Collapse All Sections" link Then I see the "Collapse All Sections" link
Scenario: Collapsing all sections when all sections are expanded Scenario: Collapsing all sections when all sections are expanded
...@@ -57,4 +58,4 @@ Feature: Overview Toggle Section ...@@ -57,4 +58,4 @@ Feature: Overview Toggle Section
When I expand the first section When I expand the first section
And I click the "Expand All Sections" link And I click the "Expand All Sections" link
Then I see the "Collapse All Sections" link Then I see the "Collapse All Sections" link
And all sections are expanded And all sections are expanded
\ No newline at end of file
...@@ -3,13 +3,15 @@ Feature: Create Subsection ...@@ -3,13 +3,15 @@ Feature: Create Subsection
As a course author As a course author
I want to create and edit subsections 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 Given I have opened a new course section in Studio
When I click the New Subsection link When I click the New Subsection link
And I enter the subsection name and click save And I enter the subsection name and click save
Then I see my subsection on the Courseware page 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 Given I have opened a new course section in Studio
When I click the New Subsection link When I click the New Subsection link
And I enter a subsection name with a quote and click save And I enter a subsection name with a quote and click save
...@@ -17,7 +19,7 @@ Feature: Create Subsection ...@@ -17,7 +19,7 @@ Feature: Create Subsection
And I click to edit the subsection name And I click to edit the subsection name
Then I see the complete subsection name with a quote in the editor 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 Given I have opened a new course section in Studio
And I have added a new subsection And I have added a new subsection
And I mark it as Homework And I mark it as Homework
...@@ -25,20 +27,19 @@ Feature: Create Subsection ...@@ -25,20 +27,19 @@ Feature: Create Subsection
And I reload the page And I reload the page
Then I see it marked as Homework 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 Given I have opened a new subsection in Studio
And I have set a release date and due date in different years And I have set a release date and due date in different years
Then I see the correct dates Then I see the correct dates
And I reload the page And I reload the page
Then I see the correct dates Then I see the correct dates
@skip-phantom @skip-phantom
Scenario: Delete a subsection Scenario: Delete a subsection
Given I have opened a new course section in Studio Given I have opened a new course section in Studio
And I have added a new subsection And I have added a new subsection
And I see my subsection on the Courseware page And I see my subsection on the Courseware page
When I press the "subsection" delete icon When I press the "subsection" delete icon
And I confirm the alert And I confirm the alert
Then the subsection does not exist Then the subsection does not exist
...@@ -59,7 +59,7 @@ class Command(BaseCommand): ...@@ -59,7 +59,7 @@ class Command(BaseCommand):
discussion_items = _get_discussion_items(course) discussion_items = _get_discussion_items(course)
# now query all discussion items via get_items() and compare with the tree-traversal # 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]) 'discussion', None, None])
for item in queried_discussion_items: for item in queried_discussion_items:
......
...@@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, CommandError ...@@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.store_utilities import clone_course from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from auth.authz import _copy_course_group from auth.authz import _copy_course_group
...@@ -16,8 +15,7 @@ from auth.authz import _copy_course_group ...@@ -16,8 +15,7 @@ from auth.authz import _copy_course_group
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = 'Clone a MongoDB backed course to another location'
'''Clone a MongoDB backed course to another location'''
def handle(self, *args, **options): def handle(self, *args, **options):
if len(args) != 2: if len(args) != 2:
......
...@@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, CommandError ...@@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.store_utilities import delete_course from xmodule.modulestore.store_utilities import delete_course
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from .prompt import query_yes_no from .prompt import query_yes_no
...@@ -38,7 +37,7 @@ class Command(BaseCommand): ...@@ -38,7 +37,7 @@ class Command(BaseCommand):
if query_yes_no("Deleting course {0}. Confirm?".format(loc_str), default="no"): 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"): if query_yes_no("Are you sure. This action cannot be undone!", default="no"):
loc = CourseDescriptor.id_to_location(loc_str) 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....' print 'removing User permissions from course....'
# in the django layer, we need to remove all the user permissions groups associated with this course # in the django layer, we need to remove all the user permissions groups associated with this course
if commit: if commit:
......
...@@ -7,7 +7,6 @@ from django.core.management.base import BaseCommand, CommandError ...@@ -7,7 +7,6 @@ from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.xml_exporter import export_to_xml from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
...@@ -15,8 +14,7 @@ unnamed_modules = 0 ...@@ -15,8 +14,7 @@ unnamed_modules = 0
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = 'Import the specified data directory into the default ModuleStore'
'''Import the specified data directory into the default ModuleStore'''
def handle(self, *args, **options): def handle(self, *args, **options):
if len(args) != 2: if len(args) != 2:
......
...@@ -12,8 +12,7 @@ unnamed_modules = 0 ...@@ -12,8 +12,7 @@ unnamed_modules = 0
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = 'Import the specified data directory into the default ModuleStore'
'''Import the specified data directory into the default ModuleStore'''
def handle(self, *args, **options): def handle(self, *args, **options):
if len(args) == 0: if len(args) == 0:
...@@ -28,4 +27,4 @@ class Command(BaseCommand): ...@@ -28,4 +27,4 @@ class Command(BaseCommand):
data=data_dir, data=data_dir,
courses=course_dirs) courses=course_dirs)
import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False, import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False,
static_content_store=contentstore(), verbose=True) static_content_store=contentstore(), verbose=True)
...@@ -11,8 +11,8 @@ def query_yes_no(question, default="yes"): ...@@ -11,8 +11,8 @@ def query_yes_no(question, default="yes"):
The "answer" return value is one of "yes" or "no". The "answer" return value is one of "yes" or "no".
""" """
valid = {"yes":True, "y":True, "ye":True, valid = {"yes": True, "y": True, "ye": True,
"no":False, "n":False} "no": False, "n": False}
if default is None: if default is None:
prompt = " [y/n] " prompt = " [y/n] "
elif default == "yes": elif default == "yes":
...@@ -30,5 +30,4 @@ def query_yes_no(question, default="yes"): ...@@ -30,5 +30,4 @@ def query_yes_no(question, default="yes"):
elif choice in valid: elif choice in valid:
return valid[choice] return valid[choice]
else: else:
sys.stdout.write("Please respond with 'yes' or 'no' "\ sys.stdout.write("Please respond with 'yes' or 'no' (or 'y' or 'n').\n")
"(or 'y' or 'n').\n")
from xmodule.templates import update_templates from xmodule.templates import update_templates
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = 'Imports and updates the Studio component templates from the code pack and put in the DB'
'''Imports and updates the Studio component templates from the code pack and put in the DB'''
def handle(self, *args, **options): def handle(self, *args, **options):
update_templates() update_templates()
\ No newline at end of file
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.xml_importer import perform_xlint from xmodule.modulestore.xml_importer import perform_xlint
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
unnamed_modules = 0 unnamed_modules = 0
...@@ -9,10 +7,11 @@ unnamed_modules = 0 ...@@ -9,10 +7,11 @@ unnamed_modules = 0
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = \
''' '''
Verify the structure of courseware as to it's suitability for import 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)] To run test: rake cms:xlint DATA_DIR=../data [COURSE_DIR=content-edx-101 (optional parameter)]
''' '''
def handle(self, *args, **options): def handle(self, *args, **options):
if len(args) == 0: if len(args) == 0:
raise CommandError("import requires at least one argument: <data directory> [<course dir>...]") raise CommandError("import requires at least one argument: <data directory> [<course dir>...]")
......
...@@ -6,18 +6,18 @@ from django.conf import settings ...@@ -6,18 +6,18 @@ from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from path import path from path import path
from tempdir import mkdtemp_clean from tempdir import mkdtemp_clean
from datetime import timedelta
import json
from fs.osfs import OSFS from fs.osfs import OSFS
import copy import copy
from json import loads from json import loads
import traceback import traceback
from datetime import timedelta
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.dispatch import Signal from django.dispatch import Signal
from contentstore.utils import get_modulestore 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.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -39,6 +39,7 @@ TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE) ...@@ -39,6 +39,7 @@ TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data') TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
class MongoCollectionFindWrapper(object): class MongoCollectionFindWrapper(object):
def __init__(self, original): def __init__(self, original):
self.original = original self.original = original
...@@ -48,6 +49,7 @@ class MongoCollectionFindWrapper(object): ...@@ -48,6 +49,7 @@ class MongoCollectionFindWrapper(object):
self.counter = self.counter+1 self.counter = self.counter+1
return self.original(query, *args, **kwargs) return self.original(query, *args, **kwargs)
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
class ContentStoreToyCourseTest(ModuleStoreTestCase): class ContentStoreToyCourseTest(ModuleStoreTestCase):
""" """
...@@ -187,32 +189,37 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -187,32 +189,37 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_get_depth_with_drafts(self): def test_get_depth_with_drafts(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple']) import_from_xml(modulestore(), 'common/test/data/', ['simple'])
course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple', course = modulestore('draft').get_item(
'course', '2012_Fall', None]), depth=None) Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]),
depth=None
)
# make sure no draft items have been returned # make sure no draft items have been returned
num_drafts = self._get_draft_counts(course) num_drafts = self._get_draft_counts(course)
self.assertEqual(num_drafts, 0) self.assertEqual(num_drafts, 0)
problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple', problem = modulestore('draft').get_item(
'problem', 'ps01-simple', None])) Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None])
)
# put into draft # put into draft
modulestore('draft').clone_item(problem.location, problem.location) modulestore('draft').clone_item(problem.location, problem.location)
# make sure we can query that item and verify that it is a draft # make sure we can query that item and verify that it is a draft
draft_problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple', draft_problem = modulestore('draft').get_item(
'problem', 'ps01-simple', None])) Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None])
self.assertTrue(getattr(draft_problem,'is_draft', False)) )
self.assertTrue(getattr(draft_problem, 'is_draft', False))
#now requery with depth #now requery with depth
course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple', course = modulestore('draft').get_item(
'course', '2012_Fall', None]), depth=None) Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]),
depth=None
)
# make sure just one draft item have been returned # make sure just one draft item have been returned
num_drafts = self._get_draft_counts(course) num_drafts = self._get_draft_counts(course)
self.assertEqual(num_drafts, 1) self.assertEqual(num_drafts, 1)
def test_static_tab_reordering(self): def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
...@@ -267,9 +274,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -267,9 +274,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# make sure the parent no longer points to the child object which was deleted # make sure the parent no longer points to the child object which was deleted
self.assertTrue(sequential.location.url() in chapter.children) 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'}), json.dumps({'id': sequential.location.url(), 'delete_children': 'true', 'delete_all_versions': 'true'}),
"application/json") "application/json"
)
found = False found = False
try: try:
...@@ -386,8 +395,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -386,8 +395,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
draft_store.clone_item(vertical.location, vertical.location) 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(): 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()) root_dir = path(mkdtemp_clean())
...@@ -402,7 +415,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -402,7 +415,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
module_store.update_children(sequential.location, sequential.children + module_store.update_children(sequential.location, sequential.children +
[private_location_no_draft.url()]) [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 = module_store.get_item(Location(['i4x', 'edX', 'full',
'sequential', 'Administrivia_and_Circuit_Elements', None])) 'sequential', 'Administrivia_and_Circuit_Elements', None]))
...@@ -533,15 +546,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -533,15 +546,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
print 'Exporting to tempdir = {0}'.format(root_dir) print 'Exporting to tempdir = {0}'.format(root_dir)
# export out to a tempdir # export out to a tempdir
exported = False export_to_xml(module_store, content_store, location, root_dir, 'test_export')
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)
class ContentStoreTest(ModuleStoreTestCase): class ContentStoreTest(ModuleStoreTestCase):
...@@ -621,10 +626,12 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -621,10 +626,12 @@ class ContentStoreTest(ModuleStoreTestCase):
"""Test viewing the index page with no courses""" """Test viewing the index page with no courses"""
# Create a course so there is something to view # Create a course so there is something to view
resp = self.client.get(reverse('index')) resp = self.client.get(reverse('index'))
self.assertContains(resp, self.assertContains(
resp,
'<h1 class="title-1">My Courses</h1>', '<h1 class="title-1">My Courses</h1>',
status_code=200, status_code=200,
html=True) html=True
)
def test_course_factory(self): def test_course_factory(self):
"""Test that the course factory works correctly.""" """Test that the course factory works correctly."""
...@@ -641,10 +648,12 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -641,10 +648,12 @@ class ContentStoreTest(ModuleStoreTestCase):
"""Test viewing the index page with an existing course""" """Test viewing the index page with an existing course"""
CourseFactory.create(display_name='Robot Super Educational Course') CourseFactory.create(display_name='Robot Super Educational Course')
resp = self.client.get(reverse('index')) resp = self.client.get(reverse('index'))
self.assertContains(resp, self.assertContains(
resp,
'<span class="class-name">Robot Super Educational Course</span>', '<span class="class-name">Robot Super Educational Course</span>',
status_code=200, status_code=200,
html=True) html=True
)
def test_course_overview_view_with_course(self): def test_course_overview_view_with_course(self):
"""Test viewing the course overview page with an existing course""" """Test viewing the course overview page with an existing course"""
...@@ -657,10 +666,12 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -657,10 +666,12 @@ class ContentStoreTest(ModuleStoreTestCase):
} }
resp = self.client.get(reverse('course_index', kwargs=data)) resp = self.client.get(reverse('course_index', kwargs=data))
self.assertContains(resp, self.assertContains(
resp,
'<article class="courseware-overview" data-course-id="i4x://MITx/999/course/Robot_Super_Course">', '<article class="courseware-overview" data-course-id="i4x://MITx/999/course/Robot_Super_Course">',
status_code=200, status_code=200,
html=True) html=True
)
def test_clone_item(self): def test_clone_item(self):
"""Test cloning an item. E.g. creating a new section""" """Test cloning an item. E.g. creating a new section"""
...@@ -676,8 +687,10 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -676,8 +687,10 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
data = parse_json(resp) data = parse_json(resp)
self.assertRegexpMatches(data['id'], self.assertRegexpMatches(
'^i4x:\/\/MITx\/999\/chapter\/([0-9]|[a-f]){32}$') data['id'],
r"^i4x://MITx/999/chapter/([0-9]|[a-f]){32}$"
)
def test_capa_module(self): def test_capa_module(self):
"""Test that a problem treats markdown specially.""" """Test that a problem treats markdown specially."""
......
...@@ -23,14 +23,14 @@ class CachingTestCase(TestCase): ...@@ -23,14 +23,14 @@ class CachingTestCase(TestCase):
def test_put_and_get(self): def test_put_and_get(self):
set_cached_content(self.mockAsset) set_cached_content(self.mockAsset)
self.assertEqual(self.mockAsset.content, get_cached_content(self.unicodeLocation).content, 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, 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): def test_delete(self):
set_cached_content(self.mockAsset) set_cached_content(self.mockAsset)
del_cached_content(self.nonUnicodeLocation) del_cached_content(self.nonUnicodeLocation)
self.assertEqual(None, get_cached_content(self.unicodeLocation), 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), self.assertEqual(None, get_cached_content(self.nonUnicodeLocation),
'should not be stored in cache with nonUnicodeLocation') 'should not be stored in cache with nonUnicodeLocation')
...@@ -8,12 +8,11 @@ from django.core.urlresolvers import reverse ...@@ -8,12 +8,11 @@ from django.core.urlresolvers import reverse
from django.utils.timezone import UTC from django.utils.timezone import UTC
from xmodule.modulestore import Location from xmodule.modulestore import Location
from models.settings.course_details import (CourseDetails, from models.settings.course_details import (CourseDetails, CourseSettingsEncoder)
CourseSettingsEncoder)
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
from contentstore.utils import get_modulestore 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 xmodule.modulestore.tests.factories import CourseFactory
from models.settings.course_metadata import CourseMetadata from models.settings.course_metadata import CourseMetadata
...@@ -21,6 +20,7 @@ from xmodule.modulestore.xml_importer import import_from_xml ...@@ -21,6 +20,7 @@ from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.fields import Date from xmodule.fields import Date
class CourseTestCase(ModuleStoreTestCase): class CourseTestCase(ModuleStoreTestCase):
def setUp(self): def setUp(self):
""" """
...@@ -47,12 +47,8 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -47,12 +47,8 @@ class CourseTestCase(ModuleStoreTestCase):
self.client = Client() self.client = Client()
self.client.login(username=uname, password=password) self.client.login(username=uname, password=password)
t = 'i4x://edx/templates/course/Empty' course = CourseFactory.create(template='i4x://edx/templates/course/Empty', org='MITx', number='999', display_name='Robot Super Course')
o = 'MITx' self.course_location = course.location
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)
class CourseDetailsTestCase(CourseTestCase): class CourseDetailsTestCase(CourseTestCase):
...@@ -86,17 +82,25 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -86,17 +82,25 @@ class CourseDetailsTestCase(CourseTestCase):
jsondetails = CourseDetails.fetch(self.course_location) jsondetails = CourseDetails.fetch(self.course_location)
jsondetails.syllabus = "<a href='foo'>bar</a>" jsondetails.syllabus = "<a href='foo'>bar</a>"
# encode - decode to convert date fields and other data which changes form # encode - decode to convert date fields and other data which changes form
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).syllabus, self.assertEqual(
jsondetails.syllabus, "After set syllabus") CourseDetails.update_from_json(jsondetails.__dict__).syllabus,
jsondetails.syllabus, "After set syllabus"
)
jsondetails.overview = "Overview" jsondetails.overview = "Overview"
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).overview, self.assertEqual(
jsondetails.overview, "After set overview") CourseDetails.update_from_json(jsondetails.__dict__).overview,
jsondetails.overview, "After set overview"
)
jsondetails.intro_video = "intro_video" jsondetails.intro_video = "intro_video"
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).intro_video, self.assertEqual(
jsondetails.intro_video, "After set intro_video") CourseDetails.update_from_json(jsondetails.__dict__).intro_video,
jsondetails.intro_video, "After set intro_video"
)
jsondetails.effort = "effort" jsondetails.effort = "effort"
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).effort, self.assertEqual(
jsondetails.effort, "After set effort") CourseDetails.update_from_json(jsondetails.__dict__).effort,
jsondetails.effort, "After set effort"
)
class CourseDetailsViewTest(CourseTestCase): class CourseDetailsViewTest(CourseTestCase):
...@@ -150,9 +154,7 @@ class CourseDetailsViewTest(CourseTestCase): ...@@ -150,9 +154,7 @@ class CourseDetailsViewTest(CourseTestCase):
@staticmethod @staticmethod
def struct_to_datetime(struct_time): def struct_to_datetime(struct_time):
return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, return datetime.datetime(*struct_time[:6], tzinfo=UTC())
struct_time.tm_mday, struct_time.tm_hour,
struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC())
def compare_date_fields(self, details, encoded, context, field): def compare_date_fields(self, details, encoded, context, field):
if details[field] is not None: if details[field] is not None:
...@@ -249,6 +251,7 @@ class CourseGradingTest(CourseTestCase): ...@@ -249,6 +251,7 @@ class CourseGradingTest(CourseTestCase):
altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1]) 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") self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2")
class CourseMetadataEditingTest(CourseTestCase): class CourseMetadataEditingTest(CourseTestCase):
def setUp(self): def setUp(self):
CourseTestCase.setUp(self) CourseTestCase.setUp(self)
...@@ -256,7 +259,6 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -256,7 +259,6 @@ class CourseMetadataEditingTest(CourseTestCase):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
self.fullcourse_location = Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]) self.fullcourse_location = Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])
def test_fetch_initial_fields(self): def test_fetch_initial_fields(self):
test_model = CourseMetadata.fetch(self.course_location) test_model = CourseMetadata.fetch(self.course_location)
self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertIn('display_name', test_model, 'Missing editable metadata field')
...@@ -271,18 +273,20 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -271,18 +273,20 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertIn('xqa_key', test_model, 'xqa_key field ') self.assertIn('xqa_key', test_model, 'xqa_key field ')
def test_update_from_json(self): def test_update_from_json(self):
test_model = CourseMetadata.update_from_json(self.course_location, test_model = CourseMetadata.update_from_json(self.course_location, {
{ "advertised_start" : "start A", "advertised_start": "start A",
"testcenter_info" : { "c" : "test" }, "testcenter_info": {"c": "test"},
"days_early_for_beta" : 2}) "days_early_for_beta": 2
})
self.update_check(test_model) self.update_check(test_model)
# try fresh fetch to ensure persistence # try fresh fetch to ensure persistence
test_model = CourseMetadata.fetch(self.course_location) test_model = CourseMetadata.fetch(self.course_location)
self.update_check(test_model) self.update_check(test_model)
# now change some of the existing metadata # now change some of the existing metadata
test_model = CourseMetadata.update_from_json(self.course_location, test_model = CourseMetadata.update_from_json(self.course_location, {
{ "advertised_start" : "start B", "advertised_start": "start B",
"display_name" : "jolly roger"}) "display_name": "jolly roger"}
)
self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value") self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value")
self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field') self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
...@@ -294,13 +298,12 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -294,13 +298,12 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field') 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.assertEqual(test_model['advertised_start'], 'start A', "advertised_start not expected value")
self.assertIn('testcenter_info', test_model, 'Missing testcenter_info metadata field') 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.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") self.assertEqual(test_model['days_early_for_beta'], 2, "days_early_for_beta not expected value")
def test_delete_key(self): 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 # ensure no harm
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in') self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
self.assertIn('display_name', test_model, 'full missing editable metadata field') self.assertIn('display_name', test_model, 'full missing editable metadata field')
......
...@@ -3,7 +3,7 @@ from contentstore import utils ...@@ -3,7 +3,7 @@ from contentstore import utils
import mock import mock
from django.test import TestCase from django.test import TestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from .utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
class LMSLinksTestCase(TestCase): class LMSLinksTestCase(TestCase):
...@@ -30,7 +30,7 @@ class LMSLinksTestCase(TestCase): ...@@ -30,7 +30,7 @@ class LMSLinksTestCase(TestCase):
class UrlReverseTestCase(ModuleStoreTestCase): class UrlReverseTestCase(ModuleStoreTestCase):
""" Tests for get_url_reverse """ """ Tests for get_url_reverse """
def test_CoursePageNames(self): def test_course_page_names(self):
""" Test the defined course pages. """ """ Test the defined course pages. """
course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course') course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course')
...@@ -69,4 +69,4 @@ class UrlReverseTestCase(ModuleStoreTestCase): ...@@ -69,4 +69,4 @@ class UrlReverseTestCase(ModuleStoreTestCase):
self.assertEquals( self.assertEquals(
'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', '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) 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
import json
import shutil
from django.test.client import Client from django.test.client import Client
from django.conf import settings
from django.core.urlresolvers import reverse 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 .utils import parse_json, user, registration
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
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
class ContentStoreTestCase(ModuleStoreTestCase): class ContentStoreTestCase(ModuleStoreTestCase):
...@@ -84,6 +62,7 @@ class ContentStoreTestCase(ModuleStoreTestCase): ...@@ -84,6 +62,7 @@ class ContentStoreTestCase(ModuleStoreTestCase):
# Now make sure that the user is now actually activated # Now make sure that the user is now actually activated
self.assertTrue(user(email).is_active) self.assertTrue(user(email).is_active)
class AuthTestCase(ContentStoreTestCase): class AuthTestCase(ContentStoreTestCase):
"""Check that various permissions-related things work""" """Check that various permissions-related things work"""
...@@ -101,9 +80,9 @@ class AuthTestCase(ContentStoreTestCase): ...@@ -101,9 +80,9 @@ class AuthTestCase(ContentStoreTestCase):
def test_public_pages_load(self): def test_public_pages_load(self):
"""Make sure pages that don't require login load without error.""" """Make sure pages that don't require login load without error."""
pages = ( pages = (
reverse('login'), reverse('login'),
reverse('signup'), reverse('signup'),
) )
for page in pages: for page in pages:
print "Checking '{0}'".format(page) print "Checking '{0}'".format(page)
self.check_page_get(page, 200) self.check_page_get(page, 200)
...@@ -136,13 +115,13 @@ class AuthTestCase(ContentStoreTestCase): ...@@ -136,13 +115,13 @@ class AuthTestCase(ContentStoreTestCase):
"""Make sure pages that do require login work.""" """Make sure pages that do require login work."""
auth_pages = ( auth_pages = (
reverse('index'), reverse('index'),
) )
# These are pages that should just load when the user is logged in # These are pages that should just load when the user is logged in
# (no data needed) # (no data needed)
simple_auth_pages = ( simple_auth_pages = (
reverse('index'), reverse('index'),
) )
# need an activated user # need an activated user
self.test_create_account() self.test_create_account()
......
...@@ -2,112 +2,11 @@ ...@@ -2,112 +2,11 @@
Utilities for contentstore tests Utilities for contentstore tests
''' '''
#pylint: disable=W0603
import json import json
import copy
from uuid import uuid4
from django.test import TestCase
from django.conf import settings
from student.models import Registration from student.models import Registration
from django.contrib.auth.models import User 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): def parse_json(response):
"""Parse response, which is assumed to be json""" """Parse response, which is assumed to be json"""
......
...@@ -346,14 +346,14 @@ def edit_unit(request, location): ...@@ -346,14 +346,14 @@ def edit_unit(request, location):
'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_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
preview_lms_base=preview_lms_base, preview_lms_base=preview_lms_base,
lms_base=settings.LMS_BASE, lms_base=settings.LMS_BASE,
org=course.location.org, org=course.location.org,
course=course.location.course, course=course.location.course,
course_name=course.location.name, course_name=course.location.name,
section=containing_section.location.name, section=containing_section.location.name,
subsection=containing_subsection.location.name, subsection=containing_subsection.location.name,
index=index) index=index)
unit_state = compute_unit_state(item) unit_state = compute_unit_state(item)
...@@ -839,6 +839,7 @@ def upload_asset(request, org, course, coursename): ...@@ -839,6 +839,7 @@ def upload_asset(request, org, course, coursename):
response['asset_url'] = StaticContent.get_url_path_from_location(content.location) response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
return response return response
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def manage_users(request, location): def manage_users(request, location):
......
...@@ -36,3 +36,4 @@ DATABASES = { ...@@ -36,3 +36,4 @@ DATABASES = {
INSTALLED_APPS += ('lettuce.django',) INSTALLED_APPS += ('lettuce.django',)
LETTUCE_APPS = ('contentstore',) LETTUCE_APPS = ('contentstore',)
LETTUCE_SERVER_PORT = 8001 LETTUCE_SERVER_PORT = 8001
LETTUCE_BROWSER = 'chrome'
...@@ -13,7 +13,6 @@ from path import path ...@@ -13,7 +13,6 @@ from path import path
# Nose Test Runner # Nose Test Runner
INSTALLED_APPS += ('django_nose',) INSTALLED_APPS += ('django_nose',)
NOSE_ARGS = ['--with-xunit']
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
TEST_ROOT = path('test_root') TEST_ROOT = path('test_root')
...@@ -28,7 +27,7 @@ GITHUB_REPO_ROOT = TEST_ROOT / "data" ...@@ -28,7 +27,7 @@ GITHUB_REPO_ROOT = TEST_ROOT / "data"
COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data" COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data"
# Makes the tests run much faster... # 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 # 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 = [ STATICFILES_DIRS = [
...@@ -41,7 +40,7 @@ STATICFILES_DIRS += [ ...@@ -41,7 +40,7 @@ STATICFILES_DIRS += [
if os.path.isdir(COMMON_TEST_DATA_ROOT / course_dir) if os.path.isdir(COMMON_TEST_DATA_ROOT / course_dir)
] ]
modulestore_options = { MODULESTORE_OPTIONS = {
'default_class': 'xmodule.raw_module.RawDescriptor', 'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost', 'host': 'localhost',
'db': 'test_xmodule', 'db': 'test_xmodule',
...@@ -53,15 +52,15 @@ modulestore_options = { ...@@ -53,15 +52,15 @@ modulestore_options = {
MODULESTORE = { MODULESTORE = {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options 'OPTIONS': MODULESTORE_OPTIONS
}, },
'direct': { 'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options 'OPTIONS': MODULESTORE_OPTIONS
}, },
'draft': { 'draft': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': modulestore_options 'OPTIONS': MODULESTORE_OPTIONS
} }
} }
...@@ -76,7 +75,7 @@ CONTENTSTORE = { ...@@ -76,7 +75,7 @@ CONTENTSTORE = {
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "cms.db", 'NAME': TEST_ROOT / "db" / "cms.db",
}, },
} }
...@@ -121,3 +120,7 @@ PASSWORD_HASHERS = ( ...@@ -121,3 +120,7 @@ PASSWORD_HASHERS = (
# dummy segment-io key # dummy segment-io key
SEGMENT_IO_KEY = '***REMOVED***' SEGMENT_IO_KEY = '***REMOVED***'
# disable NPS survey in test mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
...@@ -87,12 +87,12 @@ from contentstore import utils ...@@ -87,12 +87,12 @@ from contentstore import utils
<div class="note note-promotion note-promotion-courseURL has-actions"> <div class="note note-promotion note-promotion-courseURL has-actions">
<h3 class="title">Course Summary Page <span class="tip">(for student enrollment and access)</span></h3> <h3 class="title">Course Summary Page <span class="tip">(for student enrollment and access)</span></h3>
<div class="copy"> <div class="copy">
<p><a class="link-courseURL" rel="external" href="${utils.get_lms_link_for_about_page(course_location)}" />${utils.get_lms_link_for_about_page(course_location)}</a></p> <p><a class="link-courseURL" rel="external" href="https:${utils.get_lms_link_for_about_page(course_location)}" />https:${utils.get_lms_link_for_about_page(course_location)}</a></p>
</div> </div>
<ul class="list-actions"> <ul class="list-actions">
<li class="action-item"> <li class="action-item">
<a title="Send a note to students via email" href="mailto:john.doe@gmail.com?Subject=Enroll%20in%20COURSENAME&body=Hi,%20COURSENAME,%20provided%20by%20edX,%20is%20almost%20ready%20to%20begin.%20Please%20enroll%20for%20this%20course%20at%20${utils.get_lms_link_for_about_page(course_location)}." class="action action-primary"><i class="ss-icon icon ss-symbolicons-standard icon icon-inline icon-announcement">&#x2709;</i> Send an invitation to your students</a> <a title="Send a note to students via email" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20&quot;${context_course.display_name_with_default}&quot;,%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="ss-icon icon ss-symbolicons-standard icon icon-inline icon-announcement">&#x2709;</i> Invite your students</a>
</li> </li>
</ul> </ul>
</div> </div>
......
...@@ -2,17 +2,17 @@ from student.models import (User, UserProfile, Registration, ...@@ -2,17 +2,17 @@ from student.models import (User, UserProfile, Registration,
CourseEnrollmentAllowed, CourseEnrollment) CourseEnrollmentAllowed, CourseEnrollment)
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from datetime import datetime from datetime import datetime
from factory import Factory, SubFactory from factory import DjangoModelFactory, Factory, SubFactory, PostGenerationMethodCall
from uuid import uuid4 from uuid import uuid4
class GroupFactory(Factory): class GroupFactory(DjangoModelFactory):
FACTORY_FOR = Group FACTORY_FOR = Group
name = 'staff_MITx/999/Robot_Super_Course' name = 'staff_MITx/999/Robot_Super_Course'
class UserProfileFactory(Factory): class UserProfileFactory(DjangoModelFactory):
FACTORY_FOR = UserProfile FACTORY_FOR = UserProfile
user = None user = None
...@@ -23,19 +23,20 @@ class UserProfileFactory(Factory): ...@@ -23,19 +23,20 @@ class UserProfileFactory(Factory):
goals = 'World domination' goals = 'World domination'
class RegistrationFactory(Factory): class RegistrationFactory(DjangoModelFactory):
FACTORY_FOR = Registration FACTORY_FOR = Registration
user = None user = None
activation_key = uuid4().hex activation_key = uuid4().hex
class UserFactory(Factory): class UserFactory(DjangoModelFactory):
FACTORY_FOR = User FACTORY_FOR = User
username = 'robot' username = 'robot'
email = 'robot+test@edx.org' email = 'robot+test@edx.org'
password = 'test' password = PostGenerationMethodCall('set_password',
'test')
first_name = 'Robot' first_name = 'Robot'
last_name = 'Test' last_name = 'Test'
is_staff = False is_staff = False
...@@ -45,14 +46,18 @@ class UserFactory(Factory): ...@@ -45,14 +46,18 @@ class UserFactory(Factory):
date_joined = datetime(2011, 1, 1) date_joined = datetime(2011, 1, 1)
class CourseEnrollmentFactory(Factory): class AdminFactory(UserFactory):
is_staff = True
class CourseEnrollmentFactory(DjangoModelFactory):
FACTORY_FOR = CourseEnrollment FACTORY_FOR = CourseEnrollment
user = SubFactory(UserFactory) user = SubFactory(UserFactory)
course_id = 'edX/toy/2012_Fall' course_id = 'edX/toy/2012_Fall'
class CourseEnrollmentAllowedFactory(Factory): class CourseEnrollmentAllowedFactory(DjangoModelFactory):
FACTORY_FOR = CourseEnrollmentAllowed FACTORY_FOR = CourseEnrollmentAllowed
email = 'test@edx.org' email = 'test@edx.org'
......
...@@ -1079,7 +1079,7 @@ def test_center_login(request): ...@@ -1079,7 +1079,7 @@ def test_center_login(request):
# which contains the error code describing the exceptional condition. # which contains the error code describing the exceptional condition.
def makeErrorURL(error_url, error_code): def makeErrorURL(error_url, error_code):
log.error("generating error URL with error code {}".format(error_code)) log.error("generating error URL with error code {}".format(error_code))
return "{}?code={}".format(error_url, error_code); return "{}?code={}".format(error_url, error_code)
# get provided error URL, which will be used as a known prefix for returning error messages to the # get provided error URL, which will be used as a known prefix for returning error messages to the
# Pearson shell. # Pearson shell.
...@@ -1088,7 +1088,7 @@ def test_center_login(request): ...@@ -1088,7 +1088,7 @@ def test_center_login(request):
# TODO: check that the parameters have not been tampered with, by comparing the code provided by Pearson # 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. # with the code we calculate for the same parameters.
if 'code' not in request.POST: if 'code' not in request.POST:
return HttpResponseRedirect(makeErrorURL(error_url, "missingSecurityCode")); return HttpResponseRedirect(makeErrorURL(error_url, "missingSecurityCode"))
code = request.POST.get("code") code = request.POST.get("code")
# calculate SHA for query string # calculate SHA for query string
...@@ -1096,7 +1096,7 @@ def test_center_login(request): ...@@ -1096,7 +1096,7 @@ def test_center_login(request):
if 'clientCandidateID' not in request.POST: if 'clientCandidateID' not in request.POST:
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientCandidateID")); return HttpResponseRedirect(makeErrorURL(error_url, "missingClientCandidateID"))
client_candidate_id = request.POST.get("clientCandidateID") client_candidate_id = request.POST.get("clientCandidateID")
# TODO: check remaining parameters, and maybe at least log if they're not matching # TODO: check remaining parameters, and maybe at least log if they're not matching
...@@ -1109,7 +1109,7 @@ def test_center_login(request): ...@@ -1109,7 +1109,7 @@ def test_center_login(request):
testcenteruser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id) testcenteruser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id)
except TestCenterUser.DoesNotExist: except TestCenterUser.DoesNotExist:
log.error("not able to find demographics for cand ID {}".format(client_candidate_id)) log.error("not able to find demographics for cand ID {}".format(client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID")); return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID"))
# find testcenter_registration that matches the provided exam code: # find testcenter_registration that matches the provided exam code:
# Note that we could rely in future on either the registrationId or the exam code, # Note that we could rely in future on either the registrationId or the exam code,
...@@ -1120,7 +1120,7 @@ def test_center_login(request): ...@@ -1120,7 +1120,7 @@ def test_center_login(request):
# so instead of "missingExamSeriesCode", we use a valid one that is # so instead of "missingExamSeriesCode", we use a valid one that is
# inaccurate but at least distinct. (Sigh.) # inaccurate but at least distinct. (Sigh.)
log.error("missing exam series code for cand ID {}".format(client_candidate_id)) log.error("missing exam series code for cand ID {}".format(client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "missingPartnerID")); return HttpResponseRedirect(makeErrorURL(error_url, "missingPartnerID"))
exam_series_code = request.POST.get('vueExamSeriesCode') exam_series_code = request.POST.get('vueExamSeriesCode')
# special case for supporting test user: # special case for supporting test user:
if client_candidate_id == "edX003671291147" and exam_series_code != '6002x001': if client_candidate_id == "edX003671291147" and exam_series_code != '6002x001':
...@@ -1130,7 +1130,7 @@ def test_center_login(request): ...@@ -1130,7 +1130,7 @@ def test_center_login(request):
registrations = TestCenterRegistration.objects.filter(testcenter_user=testcenteruser, exam_series_code=exam_series_code) registrations = TestCenterRegistration.objects.filter(testcenter_user=testcenteruser, exam_series_code=exam_series_code)
if not registrations: if not registrations:
log.error("not able to find exam registration for exam {} and cand ID {}".format(exam_series_code, client_candidate_id)) log.error("not able to find exam registration for exam {} and cand ID {}".format(exam_series_code, client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned")); return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned"))
# TODO: figure out what to do if there are more than one registrations.... # TODO: figure out what to do if there are more than one registrations....
# for now, just take the first... # for now, just take the first...
...@@ -1140,11 +1140,11 @@ def test_center_login(request): ...@@ -1140,11 +1140,11 @@ def test_center_login(request):
course = course_from_id(course_id) # assume it will be found.... course = course_from_id(course_id) # assume it will be found....
if not course: if not course:
log.error("not able to find course from ID {} for cand ID {}".format(course_id, client_candidate_id)) log.error("not able to find course from ID {} for cand ID {}".format(course_id, client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests")); return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"))
exam = course.get_test_center_exam(exam_series_code) exam = course.get_test_center_exam(exam_series_code)
if not exam: 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)) log.error("not able to find exam {} for course ID {} and cand ID {}".format(exam_series_code, course_id, client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests")); return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"))
location = exam.exam_url 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)) log.info("proceeding with test of cand {} on exam {} for course {}: URL = {}".format(client_candidate_id, exam_series_code, course_id, location))
...@@ -1152,7 +1152,7 @@ def test_center_login(request): ...@@ -1152,7 +1152,7 @@ def test_center_login(request):
timelimit_descriptor = modulestore().get_instance(course_id, Location(location)) timelimit_descriptor = modulestore().get_instance(course_id, Location(location))
if not timelimit_descriptor: 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)) log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format(client_candidate_id, exam_series_code, course_id, location))
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram")); return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"))
timelimit_module_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user, timelimit_module_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user,
timelimit_descriptor, depth=None) timelimit_descriptor, depth=None)
...@@ -1160,11 +1160,11 @@ def test_center_login(request): ...@@ -1160,11 +1160,11 @@ def test_center_login(request):
timelimit_module_cache, course_id, position=None) timelimit_module_cache, course_id, position=None)
if not timelimit_module.category == 'timelimit': 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)) log.error("cand {} on exam {} for course {}: non-timelimit module at location {}".format(client_candidate_id, exam_series_code, course_id, location))
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram")); return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"))
if timelimit_module and timelimit_module.has_ended: 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)) log.warning("cand {} on exam {} for course {}: test already over at {}".format(client_candidate_id, exam_series_code, course_id, timelimit_module.ending_at))
return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken")); return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken"))
# check if we need to provide an accommodation: # check if we need to provide an accommodation:
time_accommodation_mapping = {'ET12ET' : 'ADDHALFTIME', time_accommodation_mapping = {'ET12ET' : 'ADDHALFTIME',
......
from lettuce import before, after, world from lettuce import before, after, world
from splinter.browser import Browser from splinter.browser import Browser
from logging import getLogger 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 # Let the LMS and CMS do their one-time setup
# For example, setting up mongo caches # For example, setting up mongo caches
...@@ -10,18 +12,14 @@ from cms import one_time_startup ...@@ -10,18 +12,14 @@ from cms import one_time_startup
logger = getLogger(__name__) logger = getLogger(__name__)
logger.info("Loading the lettuce acceptance testing terrain file...") logger.info("Loading the lettuce acceptance testing terrain file...")
from django.core.management import call_command
@before.harvest @before.harvest
def initial_setup(server): def initial_setup(server):
''' '''
Launch the browser once before executing the tests Launch the browser once before executing the tests
''' '''
# Launch the browser app (choose one of these below) browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome')
world.browser = Browser('chrome') world.browser = Browser(browser_driver)
# world.browser = Browser('phantomjs')
# world.browser = Browser('firefox')
@before.each_scenario @before.each_scenario
...@@ -34,6 +32,15 @@ def reset_data(scenario): ...@@ -34,6 +32,15 @@ def reset_data(scenario):
call_command('flush', interactive=False) 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 @after.all
def teardown_browser(total): def teardown_browser(total):
''' '''
......
...@@ -132,6 +132,8 @@ def i_am_logged_in(step): ...@@ -132,6 +132,8 @@ def i_am_logged_in(step):
world.create_user('robot') world.create_user('robot')
world.log_in('robot', 'test') world.log_in('robot', 'test')
world.browser.visit(django_url('/')) 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$') @step(u'I am an edX user$')
......
...@@ -14,7 +14,7 @@ from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat ...@@ -14,7 +14,7 @@ from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload", V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload",
"skip_spelling_checks", "due", "graceperiod"] "skip_spelling_checks", "due", "graceperiod", "weight"]
V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state", V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state",
"student_attempts", "ready_to_reset"] "student_attempts", "ready_to_reset"]
...@@ -106,10 +106,10 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): ...@@ -106,10 +106,10 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
icon_class = 'problem' icon_class = 'problem'
js = {'coffee': js = {'coffee':
[resource_string(__name__, 'js/src/combinedopenended/display.coffee'), [resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'), resource_string(__name__, 'js/src/javascript_loader.coffee'),
]} ]}
js_module_name = "CombinedOpenEnded" js_module_name = "CombinedOpenEnded"
css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]} css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}
......
...@@ -92,9 +92,13 @@ class ConditionalModule(ConditionalFields, XModule): ...@@ -92,9 +92,13 @@ class ConditionalModule(ConditionalFields, XModule):
if xml_value and self.required_modules: if xml_value and self.required_modules:
for module in self.required_modules: for module in self.required_modules:
if not hasattr(module, attr_name): if not hasattr(module, attr_name):
raise Exception('Error in conditional module: \ # We don't throw an exception here because it is possible for
required module {module} has no {module_attr}'.format( # the descriptor of a required module to have a property but
module=module, module_attr=attr_name)) # for the resulting module to be a (flavor of) ErrorModule.
# So just log and return false.
log.warn('Error in conditional module: \
required module {module} has no {module_attr}'.format(module=module, module_attr=attr_name))
return False
attr = getattr(module, attr_name) attr = getattr(module, attr_name)
if callable(attr): if callable(attr):
...@@ -137,16 +141,15 @@ class ConditionalModule(ConditionalFields, XModule): ...@@ -137,16 +141,15 @@ class ConditionalModule(ConditionalFields, XModule):
def get_icon_class(self): def get_icon_class(self):
new_class = 'other' new_class = 'other'
if self.is_condition_satisfied(): # HACK: This shouldn't be hard-coded to two types
# HACK: This shouldn't be hard-coded to two types # OBSOLETE: This obsoletes 'type'
# OBSOLETE: This obsoletes 'type' class_priority = ['video', 'problem']
class_priority = ['video', 'problem']
child_classes = [self.system.get_module(child_descriptor).get_icon_class()
child_classes = [self.system.get_module(child_descriptor).get_icon_class() for child_descriptor in self.descriptor.get_children()]
for child_descriptor in self.descriptor.get_children()] for c in class_priority:
for c in class_priority: if c in child_classes:
if c in child_classes: new_class = c
new_class = c
return new_class return new_class
......
...@@ -232,6 +232,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -232,6 +232,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
# disable the syllabus content for courses that do not provide a syllabus # disable the syllabus content for courses that do not provide a syllabus
self.syllabus_present = self.system.resources_fs.exists(path('syllabus')) self.syllabus_present = self.system.resources_fs.exists(path('syllabus'))
self._grading_policy = {} self._grading_policy = {}
self.set_grading_policy(self.grading_policy) self.set_grading_policy(self.grading_policy)
self.test_center_exams = [] self.test_center_exams = []
......
...@@ -122,6 +122,7 @@ div.combined-rubric-container { ...@@ -122,6 +122,7 @@ div.combined-rubric-container {
span.rubric-category { span.rubric-category {
font-size: .9em; font-size: .9em;
font-weight: bold;
} }
padding-bottom: 5px; padding-bottom: 5px;
padding-top: 10px; padding-top: 10px;
......
...@@ -90,6 +90,7 @@ class @CombinedOpenEnded ...@@ -90,6 +90,7 @@ class @CombinedOpenEnded
@element=element @element=element
@reinitialize(element) @reinitialize(element)
$(window).keydown @keydown_handler $(window).keydown @keydown_handler
$(window).keyup @keyup_handler
reinitialize: (element) -> reinitialize: (element) ->
@wrapper=$(element).find('section.xmodule_CombinedOpenEndedModule') @wrapper=$(element).find('section.xmodule_CombinedOpenEndedModule')
...@@ -104,6 +105,7 @@ class @CombinedOpenEnded ...@@ -104,6 +105,7 @@ class @CombinedOpenEnded
@location = @el.data('location') @location = @el.data('location')
# set up handlers for click tracking # set up handlers for click tracking
Rubric.initialize(@location) Rubric.initialize(@location)
@is_ctrl = false
@allow_reset = @el.data('allow_reset') @allow_reset = @el.data('allow_reset')
@reset_button = @$('.reset-button') @reset_button = @$('.reset-button')
...@@ -322,6 +324,7 @@ class @CombinedOpenEnded ...@@ -322,6 +324,7 @@ class @CombinedOpenEnded
save_answer: (event) => save_answer: (event) =>
event.preventDefault() event.preventDefault()
max_filesize = 2*1000*1000 #2MB max_filesize = 2*1000*1000 #2MB
pre_can_upload_files = @can_upload_files
if @child_state == 'initial' if @child_state == 'initial'
files = "" files = ""
if @can_upload_files == true if @can_upload_files == true
...@@ -353,6 +356,7 @@ class @CombinedOpenEnded ...@@ -353,6 +356,7 @@ class @CombinedOpenEnded
@find_assessment_elements() @find_assessment_elements()
@rebind() @rebind()
else else
@can_upload_files = pre_can_upload_files
@gentle_alert response.error @gentle_alert response.error
$.ajaxWithPrefix("#{@ajax_url}/save_answer",settings) $.ajaxWithPrefix("#{@ajax_url}/save_answer",settings)
...@@ -360,10 +364,17 @@ class @CombinedOpenEnded ...@@ -360,10 +364,17 @@ class @CombinedOpenEnded
else else
@errors_area.html(@out_of_sync_message) @errors_area.html(@out_of_sync_message)
keydown_handler: (e) => keydown_handler: (event) =>
# only do anything when the key pressed is the 'enter' key #Previously, responses were submitted when hitting enter. Add in a modifier that ensures that ctrl+enter is needed.
if e.which == 13 && @child_state == 'assessing' && Rubric.check_complete() if event.which == 17 && @is_ctrl==false
@save_assessment(e) @is_ctrl=true
else if @is_ctrl==true && event.which == 13 && @child_state == 'assessing' && Rubric.check_complete()
@save_assessment(event)
keyup_handler: (event) =>
#Handle keyup event when ctrl key is released
if event.which == 17 && @is_ctrl==true
@is_ctrl=false
save_assessment: (event) => save_assessment: (event) =>
event.preventDefault() event.preventDefault()
...@@ -482,8 +493,10 @@ class @CombinedOpenEnded ...@@ -482,8 +493,10 @@ class @CombinedOpenEnded
if @accept_file_upload == "True" if @accept_file_upload == "True"
if window.File and window.FileReader and window.FileList and window.Blob if window.File and window.FileReader and window.FileList and window.Blob
@can_upload_files = true @can_upload_files = true
@file_upload_area.html('<input type="file" class="file-upload-box">') @file_upload_area.html('<input type="file" class="file-upload-box"><img class="file-upload-preview" src="#" alt="Uploaded image" />')
@file_upload_area.show() @file_upload_area.show()
$('.file-upload-preview').hide()
$('.file-upload-box').change @preview_image
else else
@gentle_alert 'File uploads are required for this question, but are not supported in this browser. Try the newest version of google chrome. Alternatively, if you have uploaded the image to the web, you can paste a link to it into the answer box.' @gentle_alert 'File uploads are required for this question, but are not supported in this browser. Try the newest version of google chrome. Alternatively, if you have uploaded the image to the web, you can paste a link to it into the answer box.'
...@@ -539,3 +552,28 @@ class @CombinedOpenEnded ...@@ -539,3 +552,28 @@ class @CombinedOpenEnded
log_feedback_selection: (event) -> log_feedback_selection: (event) ->
target_selection = $(event.target).val() target_selection = $(event.target).val()
Logger.log 'oe_feedback_response_selected', {value: target_selection} Logger.log 'oe_feedback_response_selected', {value: target_selection}
remove_attribute: (name) =>
if $('.file-upload-preview').attr(name)
$('.file-upload-preview')[0].removeAttribute(name)
preview_image: () =>
if $('.file-upload-box')[0].files && $('.file-upload-box')[0].files[0]
reader = new FileReader()
reader.onload = (e) =>
max_dim = 150
@remove_attribute('src')
@remove_attribute('height')
@remove_attribute('width')
$('.file-upload-preview').attr('src', e.target.result)
height_px = $('.file-upload-preview')[0].height
width_px = $('.file-upload-preview')[0].width
scale_factor = 0
if height_px>width_px
scale_factor = height_px/max_dim
else
scale_factor = width_px/max_dim
$('.file-upload-preview')[0].width = width_px/scale_factor
$('.file-upload-preview')[0].height = height_px/scale_factor
$('.file-upload-preview').show()
reader.readAsDataURL($('.file-upload-box')[0].files[0])
...@@ -161,6 +161,7 @@ class @PeerGradingProblem ...@@ -161,6 +161,7 @@ class @PeerGradingProblem
constructor: (backend) -> constructor: (backend) ->
@prompt_wrapper = $('.prompt-wrapper') @prompt_wrapper = $('.prompt-wrapper')
@backend = backend @backend = backend
@is_ctrl = false
# get the location of the problem # get the location of the problem
...@@ -183,6 +184,12 @@ class @PeerGradingProblem ...@@ -183,6 +184,12 @@ class @PeerGradingProblem
@grading_message.hide() @grading_message.hide()
@question_header = $('.question-header') @question_header = $('.question-header')
@question_header.click @collapse_question @question_header.click @collapse_question
@flag_submission_confirmation = $('.flag-submission-confirmation')
@flag_submission_confirmation_button = $('.flag-submission-confirmation-button')
@flag_submission_removal_button = $('.flag-submission-removal-button')
@flag_submission_confirmation_button.click @close_dialog_box
@flag_submission_removal_button.click @remove_flag
@grading_wrapper =$('.grading-wrapper') @grading_wrapper =$('.grading-wrapper')
@calibration_feedback_panel = $('.calibration-feedback') @calibration_feedback_panel = $('.calibration-feedback')
...@@ -212,6 +219,7 @@ class @PeerGradingProblem ...@@ -212,6 +219,7 @@ class @PeerGradingProblem
@answer_unknown_checkbox = $('.answer-unknown-checkbox') @answer_unknown_checkbox = $('.answer-unknown-checkbox')
$(window).keydown @keydown_handler $(window).keydown @keydown_handler
$(window).keyup @keyup_handler
@collapse_question() @collapse_question()
...@@ -233,9 +241,13 @@ class @PeerGradingProblem ...@@ -233,9 +241,13 @@ class @PeerGradingProblem
@calibration_interstitial_page.hide() @calibration_interstitial_page.hide()
@is_calibrated_check() @is_calibrated_check()
@flag_student_checkbox.click =>
@flag_box_checked()
@calibration_feedback_button.hide() @calibration_feedback_button.hide()
@calibration_feedback_panel.hide() @calibration_feedback_panel.hide()
@error_container.hide() @error_container.hide()
@flag_submission_confirmation.hide()
@is_calibrated_check() @is_calibrated_check()
...@@ -283,6 +295,17 @@ class @PeerGradingProblem ...@@ -283,6 +295,17 @@ class @PeerGradingProblem
# #
########## ##########
remove_flag: () =>
@flag_student_checkbox.removeAttr("checked")
@close_dialog_box()
close_dialog_box: () =>
$( ".flag-submission-confirmation" ).dialog('close')
flag_box_checked: () =>
if @flag_student_checkbox.is(':checked')
$( ".flag-submission-confirmation" ).dialog({ height: 400, width: 400 })
# called after we perform an is_student_calibrated check # called after we perform an is_student_calibrated check
calibration_check_callback: (response) => calibration_check_callback: (response) =>
if response.success if response.success
...@@ -338,13 +361,19 @@ class @PeerGradingProblem ...@@ -338,13 +361,19 @@ class @PeerGradingProblem
@grade = Rubric.get_total_score() @grade = Rubric.get_total_score()
keydown_handler: (event) => keydown_handler: (event) =>
if event.which == 13 && @submit_button.is(':visible') #Previously, responses were submitted when hitting enter. Add in a modifier that ensures that ctrl+enter is needed.
if event.which == 17 && @is_ctrl==false
@is_ctrl=true
else if event.which == 13 && @submit_button.is(':visible') && @is_ctrl==true
if @calibration if @calibration
@submit_calibration_essay() @submit_calibration_essay()
else else
@submit_grade() @submit_grade()
keyup_handler: (event) =>
#Handle keyup event when ctrl key is released
if event.which == 17 && @is_ctrl==true
@is_ctrl=false
########## ##########
...@@ -443,7 +472,6 @@ class @PeerGradingProblem ...@@ -443,7 +472,6 @@ class @PeerGradingProblem
calibration_wrapper = $('.calibration-feedback-wrapper') calibration_wrapper = $('.calibration-feedback-wrapper')
calibration_wrapper.html("<p>The score you gave was: #{@grade}. The actual score is: #{response.actual_score}</p>") calibration_wrapper.html("<p>The score you gave was: #{@grade}. The actual score is: #{response.actual_score}</p>")
score = parseInt(@grade) score = parseInt(@grade)
actual_score = parseInt(response.actual_score) actual_score = parseInt(response.actual_score)
...@@ -452,6 +480,11 @@ class @PeerGradingProblem ...@@ -452,6 +480,11 @@ class @PeerGradingProblem
else else
calibration_wrapper.append("<p>You may want to review the rubric again.</p>") calibration_wrapper.append("<p>You may want to review the rubric again.</p>")
if response.actual_rubric != undefined
calibration_wrapper.append("<div>Instructor Scored Rubric: #{response.actual_rubric}</div>")
if response.actual_feedback!=undefined
calibration_wrapper.append("<div>Instructor Feedback: #{response.actual_feedback}</div>")
# disable score selection and submission from the grading interface # disable score selection and submission from the grading interface
$("input[name='score-selection']").attr('disabled', true) $("input[name='score-selection']").attr('disabled', true)
@submit_button.hide() @submit_button.hide()
......
import copy
from uuid import uuid4
from django.test import TestCase
from django.conf import settings
import xmodule.modulestore.django
from xmodule.templates import update_templates
class ModuleStoreTestCase(TestCase):
""" Subclass for any test case that uses the mongodb
module store. This populates a uniquely named modulestore
collection with templates before running the TestCase
and drops it they are finished. """
@staticmethod
def flush_mongo_except_templates():
'''
Delete everything in the module store except templates
'''
modulestore = xmodule.modulestore.django.modulestore()
# This query means: every item in the collection
# that is not a template
query = {"_id.course": {"$ne": "templates"}}
# Remove everything except templates
modulestore.collection.remove(query)
@staticmethod
def load_templates_if_necessary():
'''
Load templates into the modulestore only if they do not already exist.
We need the templates, because they are copied to create
XModules such as sections and problems
'''
modulestore = xmodule.modulestore.django.modulestore()
# Count the number of templates
query = {"_id.course": "templates"}
num_templates = modulestore.collection.find(query).count()
if num_templates < 1:
update_templates()
@classmethod
def setUpClass(cls):
'''
Flush the mongo store and set up templates
'''
# Use a uuid to differentiate
# the mongo collections on jenkins.
cls.orig_modulestore = copy.deepcopy(settings.MODULESTORE)
if 'direct' not in settings.MODULESTORE:
settings.MODULESTORE['direct'] = settings.MODULESTORE['default']
settings.MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
settings.MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
xmodule.modulestore.django._MODULESTORES.clear()
print settings.MODULESTORE
TestCase.setUpClass()
@classmethod
def tearDownClass(cls):
'''
Revert to the old modulestore settings
'''
# Clean up by dropping the collection
modulestore = xmodule.modulestore.django.modulestore()
modulestore.collection.drop()
xmodule.modulestore.django._MODULESTORES.clear()
# Restore the original modulestore settings
settings.MODULESTORE = cls.orig_modulestore
def _pre_setup(self):
'''
Remove everything but the templates before each test
'''
# Flush anything that is not a template
ModuleStoreTestCase.flush_mongo_except_templates()
# Check that we have templates loaded; if not, load them
ModuleStoreTestCase.load_templates_if_necessary()
# Call superclass implementation
super(ModuleStoreTestCase, self)._pre_setup()
def _post_teardown(self):
'''
Flush everything we created except the templates
'''
# Flush anything that is not a template
ModuleStoreTestCase.flush_mongo_except_templates()
# Call superclass implementation
super(ModuleStoreTestCase, self)._post_teardown()
from factory import Factory from factory import Factory, lazy_attribute_sequence, lazy_attribute
from time import gmtime from time import gmtime
from uuid import uuid4 from uuid import uuid4
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -7,21 +7,12 @@ from xmodule.timeparse import stringify_time ...@@ -7,21 +7,12 @@ from xmodule.timeparse import stringify_time
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
def XMODULE_COURSE_CREATION(class_to_create, **kwargs):
return XModuleCourseFactory._create(class_to_create, **kwargs)
def XMODULE_ITEM_CREATION(class_to_create, **kwargs):
return XModuleItemFactory._create(class_to_create, **kwargs)
class XModuleCourseFactory(Factory): class XModuleCourseFactory(Factory):
""" """
Factory for XModule courses. Factory for XModule courses.
""" """
ABSTRACT_FACTORY = True ABSTRACT_FACTORY = True
_creation_function = (XMODULE_COURSE_CREATION,)
@classmethod @classmethod
def _create(cls, target_class, *args, **kwargs): def _create(cls, target_class, *args, **kwargs):
...@@ -33,7 +24,10 @@ class XModuleCourseFactory(Factory): ...@@ -33,7 +24,10 @@ class XModuleCourseFactory(Factory):
location = Location('i4x', org, number, location = Location('i4x', org, number,
'course', Location.clean(display_name)) 'course', Location.clean(display_name))
store = modulestore('direct') try:
store = modulestore('direct')
except KeyError:
store = modulestore()
# Write the data to the mongo datastore # Write the data to the mongo datastore
new_course = store.clone_item(template, location) new_course = store.clone_item(template, location)
...@@ -52,6 +46,10 @@ class XModuleCourseFactory(Factory): ...@@ -52,6 +46,10 @@ class XModuleCourseFactory(Factory):
# Update the data in the mongo datastore # Update the data in the mongo datastore
store.update_metadata(new_course.location.url(), own_metadata(new_course)) store.update_metadata(new_course.location.url(), own_metadata(new_course))
data = kwargs.get('data')
if data is not None:
store.update_item(new_course.location, data)
return new_course return new_course
...@@ -74,7 +72,19 @@ class XModuleItemFactory(Factory): ...@@ -74,7 +72,19 @@ class XModuleItemFactory(Factory):
""" """
ABSTRACT_FACTORY = True ABSTRACT_FACTORY = True
_creation_function = (XMODULE_ITEM_CREATION,)
display_name = None
@lazy_attribute
def category(attr):
template = Location(attr.template)
return template.category
@lazy_attribute
def location(attr):
parent = Location(attr.parent_location)
dest_name = attr.display_name.replace(" ", "_") if attr.display_name is not None else uuid4().hex
return parent._replace(category=attr.category, name=dest_name)
@classmethod @classmethod
def _create(cls, target_class, *args, **kwargs): def _create(cls, target_class, *args, **kwargs):
...@@ -110,12 +120,7 @@ class XModuleItemFactory(Factory): ...@@ -110,12 +120,7 @@ class XModuleItemFactory(Factory):
# This code was based off that in cms/djangoapps/contentstore/views.py # This code was based off that in cms/djangoapps/contentstore/views.py
parent = store.get_item(parent_location) parent = store.get_item(parent_location)
# If a display name is set, use that new_item = store.clone_item(template, kwargs.get('location'))
dest_name = display_name.replace(" ", "_") if display_name is not None else uuid4().hex
dest_location = parent_location._replace(category=template.category,
name=dest_name)
new_item = store.clone_item(template, dest_location)
# replace the display name with an optional parameter passed in from the caller # replace the display name with an optional parameter passed in from the caller
if display_name is not None: if display_name is not None:
...@@ -145,4 +150,7 @@ class ItemFactory(XModuleItemFactory): ...@@ -145,4 +150,7 @@ class ItemFactory(XModuleItemFactory):
parent_location = 'i4x://MITx/999/course/Robot_Super_Course' parent_location = 'i4x://MITx/999/course/Robot_Super_Course'
template = 'i4x://edx/templates/chapter/Empty' template = 'i4x://edx/templates/chapter/Empty'
display_name = 'Section One'
@lazy_attribute_sequence
def display_name(attr, n):
return "{} {}".format(attr.category.title(), n)
...@@ -50,12 +50,14 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d ...@@ -50,12 +50,14 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
draft_course_dir = export_fs.makeopendir('drafts') draft_course_dir = export_fs.makeopendir('drafts')
for draft_vertical in draft_verticals: for draft_vertical in draft_verticals:
parent_locs = draft_modulestore.get_parent_locations(draft_vertical.location, course.location.course_id) parent_locs = draft_modulestore.get_parent_locations(draft_vertical.location, course.location.course_id)
logging.debug('parent_locs = {0}'.format(parent_locs)) # Don't try to export orphaned items.
draft_vertical.xml_attributes['parent_sequential_url'] = Location(parent_locs[0]).url() if len(parent_locs) > 0:
sequential = modulestore.get_item(Location(parent_locs[0])) logging.debug('parent_locs = {0}'.format(parent_locs))
index = sequential.children.index(draft_vertical.location.url()) draft_vertical.xml_attributes['parent_sequential_url'] = Location(parent_locs[0]).url()
draft_vertical.xml_attributes['index_in_children_list'] = str(index) sequential = modulestore.get_item(Location(parent_locs[0]))
draft_vertical.export_to_xml(draft_course_dir) index = sequential.children.index(draft_vertical.location.url())
draft_vertical.xml_attributes['index_in_children_list'] = str(index)
draft_vertical.export_to_xml(draft_course_dir)
def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix=''): def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix=''):
......
...@@ -131,6 +131,7 @@ class CombinedOpenEndedV1Module(): ...@@ -131,6 +131,7 @@ class CombinedOpenEndedV1Module():
self.state = instance_state.get('state', self.INITIAL) self.state = instance_state.get('state', self.INITIAL)
self.student_attempts = instance_state.get('student_attempts', 0) self.student_attempts = instance_state.get('student_attempts', 0)
self.weight = instance_state.get('weight', 1)
#Allow reset is true if student has failed the criteria to move to the next child task #Allow reset is true if student has failed the criteria to move to the next child task
self.ready_to_reset = instance_state.get('ready_to_reset', False) self.ready_to_reset = instance_state.get('ready_to_reset', False)
...@@ -144,7 +145,7 @@ class CombinedOpenEndedV1Module(): ...@@ -144,7 +145,7 @@ class CombinedOpenEndedV1Module():
grace_period_string = self.instance_state.get('graceperiod', None) grace_period_string = self.instance_state.get('graceperiod', None)
try: try:
self.timeinfo = TimeInfo(due_date, grace_period_string) self.timeinfo = TimeInfo(due_date, grace_period_string)
except: except Exception:
log.error("Error parsing due date information in location {0}".format(location)) log.error("Error parsing due date information in location {0}".format(location))
raise raise
self.display_due_date = self.timeinfo.display_due_date self.display_due_date = self.timeinfo.display_due_date
...@@ -362,7 +363,7 @@ class CombinedOpenEndedV1Module(): ...@@ -362,7 +363,7 @@ class CombinedOpenEndedV1Module():
# if link.startswith(XASSET_SRCREF_PREFIX): # if link.startswith(XASSET_SRCREF_PREFIX):
# Placing try except so that if the error is fixed, this code will start working again. # Placing try except so that if the error is fixed, this code will start working again.
return_html = rewrite_links(html, self.rewrite_content_links) return_html = rewrite_links(html, self.rewrite_content_links)
except: except Exception:
pass pass
return return_html return return_html
...@@ -402,6 +403,7 @@ class CombinedOpenEndedV1Module(): ...@@ -402,6 +403,7 @@ class CombinedOpenEndedV1Module():
self.static_data, instance_state=task_state) self.static_data, instance_state=task_state)
last_response = task.latest_answer() last_response = task.latest_answer()
last_score = task.latest_score() last_score = task.latest_score()
all_scores = task.all_scores()
last_post_assessment = task.latest_post_assessment(self.system) last_post_assessment = task.latest_post_assessment(self.system)
last_post_feedback = "" last_post_feedback = ""
feedback_dicts = [{}] feedback_dicts = [{}]
...@@ -417,13 +419,18 @@ class CombinedOpenEndedV1Module(): ...@@ -417,13 +419,18 @@ class CombinedOpenEndedV1Module():
else: else:
last_post_evaluation = task.format_feedback_with_evaluation(self.system, last_post_assessment) last_post_evaluation = task.format_feedback_with_evaluation(self.system, last_post_assessment)
last_post_assessment = last_post_evaluation last_post_assessment = last_post_evaluation
rubric_data = task._parse_score_msg(task.child_history[-1].get('post_assessment', ""), self.system) try:
rubric_scores = rubric_data['rubric_scores'] rubric_data = task._parse_score_msg(task.child_history[-1].get('post_assessment', ""), self.system)
grader_types = rubric_data['grader_types'] except Exception:
feedback_items = rubric_data['feedback_items'] log.debug("Could not parse rubric data from child history. "
feedback_dicts = rubric_data['feedback_dicts'] "Likely we have not yet initialized a previous step, so this is perfectly fine.")
grader_ids = rubric_data['grader_ids'] rubric_data = {}
submission_ids = rubric_data['submission_ids'] rubric_scores = rubric_data.get('rubric_scores')
grader_types = rubric_data.get('grader_types')
feedback_items = rubric_data.get('feedback_items')
feedback_dicts = rubric_data.get('feedback_dicts')
grader_ids = rubric_data.get('grader_ids')
submission_ids = rubric_data.get('submission_ids')
elif task_type == "selfassessment": elif task_type == "selfassessment":
rubric_scores = last_post_assessment rubric_scores = last_post_assessment
grader_types = ['SA'] grader_types = ['SA']
...@@ -441,7 +448,7 @@ class CombinedOpenEndedV1Module(): ...@@ -441,7 +448,7 @@ class CombinedOpenEndedV1Module():
human_state = task.HUMAN_NAMES[state] human_state = task.HUMAN_NAMES[state]
else: else:
human_state = state human_state = state
if len(grader_types) > 0: if grader_types is not None and len(grader_types) > 0:
grader_type = grader_types[0] grader_type = grader_types[0]
else: else:
grader_type = "IN" grader_type = "IN"
...@@ -454,6 +461,7 @@ class CombinedOpenEndedV1Module(): ...@@ -454,6 +461,7 @@ class CombinedOpenEndedV1Module():
last_response_dict = { last_response_dict = {
'response': last_response, 'response': last_response,
'score': last_score, 'score': last_score,
'all_scores': all_scores,
'post_assessment': last_post_assessment, 'post_assessment': last_post_assessment,
'type': task_type, 'type': task_type,
'max_score': max_score, 'max_score': max_score,
...@@ -732,10 +740,37 @@ class CombinedOpenEndedV1Module(): ...@@ -732,10 +740,37 @@ class CombinedOpenEndedV1Module():
""" """
max_score = None max_score = None
score = None score = None
if self.check_if_done_and_scored(): if self.is_scored and self.weight is not None:
last_response = self.get_last_response(self.current_task_number) #Finds the maximum score of all student attempts and keeps it.
max_score = last_response['max_score'] score_mat = []
score = last_response['score'] for i in xrange(0, len(self.task_states)):
#For each task, extract all student scores on that task (each attempt for each task)
last_response = self.get_last_response(i)
max_score = last_response.get('max_score', None)
score = last_response.get('all_scores', None)
if score is not None:
#Convert none scores and weight scores properly
for z in xrange(0, len(score)):
if score[z] is None:
score[z] = 0
score[z] *= float(self.weight)
score_mat.append(score)
if len(score_mat) > 0:
#Currently, assume that the final step is the correct one, and that those are the final scores.
#This will change in the future, which is why the machinery above exists to extract all scores on all steps
#TODO: better final score handling.
scores = score_mat[-1]
score = max(scores)
else:
score = 0
if max_score is not None:
#Weight the max score if it is not None
max_score *= float(self.weight)
else:
#Without a max_score, we cannot have a score!
score = None
score_dict = { score_dict = {
'score': score, 'score': score,
......
...@@ -72,7 +72,8 @@ class OpenEndedChild(object): ...@@ -72,7 +72,8 @@ class OpenEndedChild(object):
try: try:
instance_state = json.loads(instance_state) instance_state = json.loads(instance_state)
except: except:
log.error("Could not load instance state for open ended. Setting it to nothing.: {0}".format(instance_state)) log.error(
"Could not load instance state for open ended. Setting it to nothing.: {0}".format(instance_state))
else: else:
instance_state = {} instance_state = {}
...@@ -81,8 +82,8 @@ class OpenEndedChild(object): ...@@ -81,8 +82,8 @@ class OpenEndedChild(object):
# element. # element.
# Scores are on scale from 0 to max_score # Scores are on scale from 0 to max_score
self.child_history=instance_state.get('child_history',[]) self.child_history = instance_state.get('child_history', [])
self.child_state=instance_state.get('child_state', self.INITIAL) self.child_state = instance_state.get('child_state', self.INITIAL)
self.child_created = instance_state.get('child_created', False) self.child_created = instance_state.get('child_created', False)
self.child_attempts = instance_state.get('child_attempts', 0) self.child_attempts = instance_state.get('child_attempts', 0)
...@@ -162,6 +163,12 @@ class OpenEndedChild(object): ...@@ -162,6 +163,12 @@ class OpenEndedChild(object):
return None return None
return self.child_history[-1].get('score') return self.child_history[-1].get('score')
def all_scores(self):
"""None if not available"""
if not self.child_history:
return None
return [self.child_history[i].get('score') for i in xrange(0, len(self.child_history))]
def latest_post_assessment(self, system): def latest_post_assessment(self, system):
"""Empty string if not available""" """Empty string if not available"""
if not self.child_history: if not self.child_history:
......
...@@ -291,7 +291,7 @@ class SelfAssessmentDescriptor(): ...@@ -291,7 +291,7 @@ class SelfAssessmentDescriptor():
template_dir_name = "selfassessment" template_dir_name = "selfassessment"
def __init__(self, system): def __init__(self, system):
self.system =system self.system = system
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
......
...@@ -15,6 +15,7 @@ from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat ...@@ -15,6 +15,7 @@ from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
from xmodule.fields import Date from xmodule.fields import Date
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
from open_ended_grading_classes import combined_open_ended_rubric
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -178,8 +179,14 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -178,8 +179,14 @@ class PeerGradingModule(PeerGradingFields, XModule):
pass pass
def get_score(self): def get_score(self):
max_score = None
score = None
score_dict = {
'score': score,
'total': max_score,
}
if self.use_for_single_location not in TRUE_DICT or self.is_graded not in TRUE_DICT: if self.use_for_single_location not in TRUE_DICT or self.is_graded not in TRUE_DICT:
return None return score_dict
try: try:
count_graded = self.student_data_for_location['count_graded'] count_graded = self.student_data_for_location['count_graded']
...@@ -198,10 +205,11 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -198,10 +205,11 @@ class PeerGradingModule(PeerGradingFields, XModule):
#Ensures that once a student receives a final score for peer grading, that it does not change. #Ensures that once a student receives a final score for peer grading, that it does not change.
self.student_data_for_location = response self.student_data_for_location = response
score_dict = { if self.weight is not None:
'score': int(count_graded >= count_required and count_graded>0) * int(self.weight), score = int(count_graded >= count_required and count_graded > 0) * float(self.weight)
'total': self.max_grade * int(self.weight), total = self.max_grade * float(self.weight)
} score_dict['score'] = score
score_dict['total'] = total
return score_dict return score_dict
...@@ -384,8 +392,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -384,8 +392,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
# if we can't parse the rubric into HTML, # if we can't parse the rubric into HTML,
except etree.XMLSyntaxError: except etree.XMLSyntaxError:
#This is a dev_facing_error #This is a dev_facing_error
log.exception("Cannot parse rubric string. Raw string: {0}" log.exception("Cannot parse rubric string.")
.format(rubric))
#This is a student_facing_error #This is a student_facing_error
return {'success': False, return {'success': False,
'error': 'Error displaying submission. Please notify course staff.'} 'error': 'Error displaying submission. Please notify course staff.'}
...@@ -425,12 +432,15 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -425,12 +432,15 @@ class PeerGradingModule(PeerGradingFields, XModule):
try: try:
response = self.peer_gs.save_calibration_essay(location, grader_id, calibration_essay_id, response = self.peer_gs.save_calibration_essay(location, grader_id, calibration_essay_id,
submission_key, score, feedback, rubric_scores) submission_key, score, feedback, rubric_scores)
if 'actual_rubric' in response:
rubric_renderer = combined_open_ended_rubric.CombinedOpenEndedRubric(self.system, True)
response['actual_rubric'] = rubric_renderer.render_rubric(response['actual_rubric'])['html']
return response return response
except GradingServiceError: except GradingServiceError:
#This is a dev_facing_error #This is a dev_facing_error
log.exception( log.exception(
"Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format( "Error saving calibration grade, location: {0}, submission_key: {1}, grader_id: {2}".format(
location, submission_id, submission_key, grader_id)) location, submission_key, grader_id))
#This is a student_facing_error #This is a student_facing_error
return self._err_response('There was an error saving your score. Please notify course staff.') return self._err_response('There was an error saving your score. Please notify course staff.')
...@@ -577,5 +587,5 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor): ...@@ -577,5 +587,5 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
stores_state = True stores_state = True
has_score = True has_score = True
always_recalculate_grades=True always_recalculate_grades = True
template_dir_name = "peer_grading" template_dir_name = "peer_grading"
--- ---
metadata: metadata:
display_name: Open Ended Response display_name: Open Ended Response
max_attempts: 1 attempts: 1
is_graded: False is_graded: False
version: 1 version: 1
display_name: Open Ended Response
skip_spelling_checks: False skip_spelling_checks: False
accept_file_upload: False accept_file_upload: False
weight: "" weight: ""
......
--- ---
metadata: metadata:
display_name: Peer Grading Interface display_name: Peer Grading Interface
attempts: 1
use_for_single_location: False use_for_single_location: False
link_to_location: None link_to_location: None
is_graded: False is_graded: False
......
import json import json
from path import path
import unittest import unittest
from fs.memoryfs import MemoryFS from fs.memoryfs import MemoryFS
from ast import literal_eval
from lxml import etree
from mock import Mock, patch from mock import Mock, patch
from collections import defaultdict
from xmodule.x_module import XMLParsingSystem, XModuleDescriptor from xmodule.error_module import NonStaffErrorDescriptor
from xmodule.xml_module import is_pointer_tag
from xmodule.errortracker import make_error_tracker
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.conditional_module import ConditionalModule
from .test_export import DATA_DIR from xmodule.tests.test_export import DATA_DIR
ORG = 'test_org' ORG = 'test_org'
COURSE = 'conditional' # name of directory with course data COURSE = 'conditional' # name of directory with course data
from . import test_system from . import test_system
...@@ -47,10 +43,118 @@ class DummySystem(ImportSystem): ...@@ -47,10 +43,118 @@ class DummySystem(ImportSystem):
def render_template(self, template, context): def render_template(self, template, context):
raise Exception("Shouldn't be called") raise Exception("Shouldn't be called")
class ConditionalFactory(object):
"""
A helper class to create a conditional module and associated source and child modules
to allow for testing.
"""
@staticmethod
def create(system, source_is_error_module=False):
"""
return a dict of modules: the conditional with a single source and a single child.
Keys are 'cond_module', 'source_module', and 'child_module'.
if the source_is_error_module flag is set, create a real ErrorModule for the source.
"""
# construct source descriptor and module:
source_location = Location(["i4x", "edX", "conditional_test", "problem", "SampleProblem"])
if source_is_error_module:
# Make an error descriptor and module
source_descriptor = NonStaffErrorDescriptor.from_xml('some random xml data',
system,
org=source_location.org,
course=source_location.course,
error_msg='random error message')
source_module = source_descriptor.xmodule(system)
else:
source_descriptor = Mock()
source_descriptor.location = source_location
source_module = Mock()
# construct other descriptors:
child_descriptor = Mock()
cond_descriptor = Mock()
cond_descriptor.get_required_module_descriptors = lambda: [source_descriptor, ]
cond_descriptor.get_children = lambda: [child_descriptor, ]
cond_descriptor.xml_attributes = {"attempted": "true"}
# create child module:
child_module = Mock()
child_module.get_html = lambda: '<p>This is a secret</p>'
child_module.displayable_items = lambda: [child_module]
module_map = {source_descriptor: source_module, child_descriptor: child_module}
system.get_module = lambda descriptor: module_map[descriptor]
# construct conditional module:
cond_location = Location(["i4x", "edX", "conditional_test", "conditional", "SampleConditional"])
model_data = {'data': '<conditional/>'}
cond_module = ConditionalModule(system, cond_location, cond_descriptor, model_data)
# return dict:
return {'cond_module': cond_module,
'source_module': source_module,
'child_module': child_module }
class ConditionalModuleBasicTest(unittest.TestCase):
"""
Make sure that conditional module works, using mocks for
other modules.
"""
def setUp(self):
self.test_system = test_system()
def test_icon_class(self):
'''verify that get_icon_class works independent of condition satisfaction'''
modules = ConditionalFactory.create(self.test_system)
for attempted in ["false", "true"]:
for icon_class in [ 'other', 'problem', 'video']:
modules['source_module'].is_attempted = attempted
modules['child_module'].get_icon_class = lambda: icon_class
self.assertEqual(modules['cond_module'].get_icon_class(), icon_class)
def test_get_html(self):
modules = ConditionalFactory.create(self.test_system)
# because test_system returns the repr of the context dict passed to render_template,
# we reverse it here
html = modules['cond_module'].get_html()
html_dict = literal_eval(html)
self.assertEqual(html_dict['element_id'], 'i4x-edX-conditional_test-conditional-SampleConditional')
self.assertEqual(html_dict['id'], 'i4x://edX/conditional_test/conditional/SampleConditional')
self.assertEqual(html_dict['depends'], 'i4x-edX-conditional_test-problem-SampleProblem')
def test_handle_ajax(self):
modules = ConditionalFactory.create(self.test_system)
modules['source_module'].is_attempted = "false"
ajax = json.loads(modules['cond_module'].handle_ajax('', ''))
print "ajax: ", ajax
html = ajax['html']
self.assertFalse(any(['This is a secret' in item for item in html]))
# now change state of the capa problem to make it completed
modules['source_module'].is_attempted = "true"
ajax = json.loads(modules['cond_module'].handle_ajax('', ''))
print "post-attempt ajax: ", ajax
html = ajax['html']
self.assertTrue(any(['This is a secret' in item for item in html]))
def test_error_as_source(self):
'''
Check that handle_ajax works properly if the source is really an ErrorModule,
and that the condition is not satisfied.
'''
modules = ConditionalFactory.create(self.test_system, source_is_error_module=True)
ajax = json.loads(modules['cond_module'].handle_ajax('', ''))
html = ajax['html']
self.assertFalse(any(['This is a secret' in item for item in html]))
class ConditionalModuleTest(unittest.TestCase):
class ConditionalModuleXmlTest(unittest.TestCase):
"""
Make sure ConditionalModule works, by loading data in from an XML-defined course.
"""
@staticmethod @staticmethod
def get_system(load_error_modules=True): def get_system(load_error_modules=True):
'''Get a dummy system''' '''Get a dummy system'''
...@@ -106,7 +210,7 @@ class ConditionalModuleTest(unittest.TestCase): ...@@ -106,7 +210,7 @@ class ConditionalModuleTest(unittest.TestCase):
html_expect = "{'ajax_url': 'courses/course_id/modx/a_location', 'element_id': 'i4x-HarvardX-ER22x-conditional-condone', 'id': 'i4x://HarvardX/ER22x/conditional/condone', 'depends': 'i4x-HarvardX-ER22x-problem-choiceprob'}" html_expect = "{'ajax_url': 'courses/course_id/modx/a_location', 'element_id': 'i4x-HarvardX-ER22x-conditional-condone', 'id': 'i4x://HarvardX/ER22x/conditional/condone', 'depends': 'i4x-HarvardX-ER22x-problem-choiceprob'}"
self.assertEqual(html, html_expect) self.assertEqual(html, html_expect)
gdi = module.get_display_items() gdi = module.get_display_items()
print "gdi=", gdi print "gdi=", gdi
ajax = json.loads(module.handle_ajax('', '')) ajax = json.loads(module.handle_ajax('', ''))
...@@ -121,3 +225,4 @@ class ConditionalModuleTest(unittest.TestCase): ...@@ -121,3 +225,4 @@ class ConditionalModuleTest(unittest.TestCase):
print "post-attempt ajax: ", ajax print "post-attempt ajax: ", ajax
html = ajax['html'] html = ajax['html']
self.assertTrue(any(['This is a secret' in item for item in html])) self.assertTrue(any(['This is a secret' in item for item in html]))
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
// NOTE: // NOTE:
// Genex uses 8 global functions, all prefixed with genex: // Genex uses 8 global functions, all prefixed with genex:
// 6 are exported from GWT: // 6 are exported from GWT:
// genexSetInitialDNASequence // genexSetDefaultDNASequence
// genexSetDNASequence // genexSetDNASequence
// genexGetDNASequence // genexGetDNASequence
// genexSetClickEvent // genexSetClickEvent
...@@ -36,32 +36,35 @@ ...@@ -36,32 +36,35 @@
genexIsReady = function() { genexIsReady = function() {
var input_field = genexGetInputField(); var input_field = genexGetInputField();
var genex_saved_state = input_field.val(); var genex_saved_state = input_field.val();
var genex_initial_dna_sequence; var genex_default_dna_sequence;
var genex_dna_sequence; var genex_dna_sequence;
//Get the sequence from xml file //Get the DNA sequence from xml file
genex_initial_dna_sequence = $('#genex_dna_sequence').val(); genex_default_dna_sequence = $('#genex_dna_sequence').val();
//Call this function to set the value used by reset button //Set the default DNA
genexSetInitialDNASequence(genex_initial_dna_sequence); genexSetDefaultDNASequence(genex_default_dna_sequence);
//Now load problem
var genex_problem_number = $('#genex_problem_number').val();
genexSetProblemNumber(genex_problem_number);
//Set the DNA sequence that is displayed
if (genex_saved_state === '') { if (genex_saved_state === '') {
//Load DNA sequence from xml file //Load DNA sequence from xml file
genex_dna_sequence = genex_initial_dna_sequence; genex_dna_sequence = genex_default_dna_sequence;
} }
else { else {
//Load DNA sequence from saved value //Load DNA sequence from saved value
genex_saved_state = JSON.parse(genex_saved_state); genex_saved_state = JSON.parse(genex_saved_state);
genex_dna_sequence = genex_saved_state.genex_dna_sequence; genex_dna_sequence = genex_saved_state.genex_dna_sequence;
} }
genexSetDNASequence(genex_dna_sequence); genexSetDNASequence(genex_dna_sequence);
//Now load mouse and keyboard handlers //Now load mouse and keyboard handlers
genexSetClickEvent(); genexSetClickEvent();
genexSetKeyEvent(); genexSetKeyEvent();
//Now load problem
var genex_problem_number = $('#genex_problem_number').val();
genexSetProblemNumber(genex_problem_number);
}; };
genexStoreAnswer = function(answer) { genexStoreAnswer = function(answer) {
var input_field = genexGetInputField(); var input_field = genexGetInputField();
var value = {'genex_dna_sequence': genexGetDNASequence(), 'genex_answer': answer}; var value = {'genex_dna_sequence': genexGetDNASequence(), 'genex_answer': answer};
......
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='<script defer="defer">genex.onInjectionDone(\'genex\')<\/script>',hb='<script id="',sb='=',$='?',Ub='A016796CF7FB22261AE1160531B5CF82',ub='Bad handler "',cc='DOMContentLoaded',Vb='F28D6C3D881F6C18E3357AAB004477EF',jb='SCRIPT',gb='__gwt_marker_genex',kb='base',cb='baseUrl',T='begin',S='bootstrap',bb='clear.cache.gif',rb='content',Y='end',Kb='gecko',Lb='gecko1_8',Q='genex',Yb='genex.css',eb='genex.nocache.js',ob='genex::',U='gwt.codesvr=',V='gwt.hosted=',W='gwt.hybrid',wb='gwt:onLoadErrorFn',tb='gwt:onPropertyErrorFn',qb='gwt:property',bc='head',Ob='hosted.html?genex',ac='href',Jb='ie6',Ib='ie8',Hb='ie9',yb='iframe',ab='img',zb="javascript:''",Zb='link',Nb='loadExternalRefs',mb='meta',Bb='moduleRequested',X='moduleStartup',Gb='msie',nb='name',Db='opera',Ab='position:absolute;width:0;height:0;border:none',$b='rel',Fb='safari',db='script',Pb='selectingPermutation',R='startup',_b='stylesheet',fb='undefined',Mb='unknown',Cb='user.agent',Eb='webkit';var m=window,n=document,o=m.__gwtStatsEvent?function(a){return m.__gwtStatsEvent(a)}:null,p=m.__gwtStatsSessionId?m.__gwtStatsSessionId:null,q,r,s,t=P,u={},v=[],w=[],x=[],y=0,z,A;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:T});if(!m.__gwt_stylesLoaded){m.__gwt_stylesLoaded={}}if(!m.__gwt_scriptsLoaded){m.__gwt_scriptsLoaded={}}function B(){var b=false;try{var c=m.location.search;return (c.indexOf(U)!=-1||(c.indexOf(V)!=-1||m.external&&m.external.gwtOnLoad))&&c.indexOf(W)==-1}catch(a){}B=function(){return b};return b} function genex(){var P='',xb='" for "gwt:onLoadErrorFn"',vb='" for "gwt:onPropertyErrorFn"',ib='"><\/script>',Z='#',Xb='.cache.html',_='/',lb='//',Qb='21B31BA00E7CE7B6BD63DD13A8586A45',Rb='63308EE54E8033A708B414CAC05B0C32',Sb='7AC57DC6EC8C1D8672DDF6E6D4EF57CC',Tb='9B4F4D4EFA24CDE2E4287CC07897F249',Wb=':',pb='::',dc='<script defer="defer">genex.onInjectionDone(\'genex\')<\/script>',hb='<script id="',sb='=',$='?',Ub='A069AC107D79C29D6237614AC340F0C0',ub='Bad handler "',Vb='C6220FCC8B9234FEAD8D826A73C6D2A4',cc='DOMContentLoaded',jb='SCRIPT',gb='__gwt_marker_genex',kb='base',cb='baseUrl',T='begin',S='bootstrap',bb='clear.cache.gif',rb='content',Y='end',Kb='gecko',Lb='gecko1_8',Q='genex',Yb='genex.css',eb='genex.nocache.js',ob='genex::',U='gwt.codesvr=',V='gwt.hosted=',W='gwt.hybrid',wb='gwt:onLoadErrorFn',tb='gwt:onPropertyErrorFn',qb='gwt:property',bc='head',Ob='hosted.html?genex',ac='href',Jb='ie6',Ib='ie8',Hb='ie9',yb='iframe',ab='img',zb="javascript:''",Zb='link',Nb='loadExternalRefs',mb='meta',Bb='moduleRequested',X='moduleStartup',Gb='msie',nb='name',Db='opera',Ab='position:absolute;width:0;height:0;border:none',$b='rel',Fb='safari',db='script',Pb='selectingPermutation',R='startup',_b='stylesheet',fb='undefined',Mb='unknown',Cb='user.agent',Eb='webkit';var m=window,n=document,o=m.__gwtStatsEvent?function(a){return m.__gwtStatsEvent(a)}:null,p=m.__gwtStatsSessionId?m.__gwtStatsSessionId:null,q,r,s,t=P,u={},v=[],w=[],x=[],y=0,z,A;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:T});if(!m.__gwt_stylesLoaded){m.__gwt_stylesLoaded={}}if(!m.__gwt_scriptsLoaded){m.__gwt_scriptsLoaded={}}function B(){var b=false;try{var c=m.location.search;return (c.indexOf(U)!=-1||(c.indexOf(V)!=-1||m.external&&m.external.gwtOnLoad))&&c.indexOf(W)==-1}catch(a){}B=function(){return b};return b}
function C(){if(q&&r){var b=n.getElementById(Q);var c=b.contentWindow;if(B()){c.__gwt_getProperty=function(a){return H(a)}}genex=null;c.gwtOnLoad(z,Q,t,y);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Y})}} function C(){if(q&&r){var b=n.getElementById(Q);var c=b.contentWindow;if(B()){c.__gwt_getProperty=function(a){return H(a)}}genex=null;c.gwtOnLoad(z,Q,t,y);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Y})}}
function D(){function e(a){var b=a.lastIndexOf(Z);if(b==-1){b=a.length}var c=a.indexOf($);if(c==-1){c=a.length}var d=a.lastIndexOf(_,Math.min(c,b));return d>=0?a.substring(0,d+1):P} function D(){function e(a){var b=a.lastIndexOf(Z);if(b==-1){b=a.length}var c=a.indexOf($);if(c==-1){c=a.length}var d=a.lastIndexOf(_,Math.min(c,b));return d>=0?a.substring(0,d+1):P}
function f(a){if(a.match(/^\w+:\/\//)){}else{var b=n.createElement(ab);b.src=a+bb;a=e(b.src)}return a} function f(a){if(a.match(/^\w+:\/\//)){}else{var b=n.createElement(ab);b.src=a+bb;a=e(b.src)}return a}
...@@ -13,6 +13,6 @@ function F(a){var b=u[a];return b==null?null:b} ...@@ -13,6 +13,6 @@ function F(a){var b=u[a];return b==null?null:b}
function G(a,b){var c=x;for(var d=0,e=a.length-1;d<e;++d){c=c[a[d]]||(c[a[d]]=[])}c[a[e]]=b} function G(a,b){var c=x;for(var d=0,e=a.length-1;d<e;++d){c=c[a[d]]||(c[a[d]]=[])}c[a[e]]=b}
function H(a){var b=w[a](),c=v[a];if(b in c){return b}var d=[];for(var e in c){d[c[e]]=e}if(A){A(a,d,b)}throw null} function H(a){var b=w[a](),c=v[a];if(b in c){return b}var d=[];for(var e in c){d[c[e]]=e}if(A){A(a,d,b)}throw null}
var I;function J(){if(!I){I=true;var a=n.createElement(yb);a.src=zb;a.id=Q;a.style.cssText=Ab;a.tabIndex=-1;n.body.appendChild(a);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Bb});a.contentWindow.location.replace(t+L)}} var I;function J(){if(!I){I=true;var a=n.createElement(yb);a.src=zb;a.id=Q;a.style.cssText=Ab;a.tabIndex=-1;n.body.appendChild(a);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Bb});a.contentWindow.location.replace(t+L)}}
w[Cb]=function(){var b=navigator.userAgent.toLowerCase();var c=function(a){return parseInt(a[1])*1000+parseInt(a[2])};if(function(){return b.indexOf(Db)!=-1}())return Db;if(function(){return b.indexOf(Eb)!=-1}())return Fb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=9}())return Hb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=8}())return Ib;if(function(){var a=/msie ([0-9]+)\.([0-9]+)/.exec(b);if(a&&a.length==3)return c(a)>=6000}())return Jb;if(function(){return b.indexOf(Kb)!=-1}())return Lb;return Mb};v[Cb]={gecko1_8:0,ie6:1,ie8:2,ie9:3,opera:4,safari:5};genex.onScriptLoad=function(){if(I){r=true;C()}};genex.onInjectionDone=function(){q=true;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:Y});C()};E();D();var K;var L;if(B()){if(m.external&&(m.external.initModule&&m.external.initModule(Q))){m.location.reload();return}L=Ob;K=P}o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Pb});if(!B()){try{G([Ib],Qb);G([Fb],Rb);G([Db],Sb);G([Hb],Tb);G([Jb],Ub);G([Lb],Vb);K=x[H(Cb)];var M=K.indexOf(Wb);if(M!=-1){y=Number(K.substring(M+1));K=K.substring(0,M)}L=K+Xb}catch(a){return}}var N;function O(){if(!s){s=true;if(!__gwt_stylesLoaded[Yb]){var a=n.createElement(Zb);__gwt_stylesLoaded[Yb]=a;a.setAttribute($b,_b);a.setAttribute(ac,t+Yb);n.getElementsByTagName(bc)[0].appendChild(a)}C();if(n.removeEventListener){n.removeEventListener(cc,O,false)}if(N){clearInterval(N)}}} w[Cb]=function(){var b=navigator.userAgent.toLowerCase();var c=function(a){return parseInt(a[1])*1000+parseInt(a[2])};if(function(){return b.indexOf(Db)!=-1}())return Db;if(function(){return b.indexOf(Eb)!=-1}())return Fb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=9}())return Hb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=8}())return Ib;if(function(){var a=/msie ([0-9]+)\.([0-9]+)/.exec(b);if(a&&a.length==3)return c(a)>=6000}())return Jb;if(function(){return b.indexOf(Kb)!=-1}())return Lb;return Mb};v[Cb]={gecko1_8:0,ie6:1,ie8:2,ie9:3,opera:4,safari:5};genex.onScriptLoad=function(){if(I){r=true;C()}};genex.onInjectionDone=function(){q=true;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:Y});C()};E();D();var K;var L;if(B()){if(m.external&&(m.external.initModule&&m.external.initModule(Q))){m.location.reload();return}L=Ob;K=P}o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Pb});if(!B()){try{G([Lb],Qb);G([Ib],Rb);G([Db],Sb);G([Jb],Tb);G([Fb],Ub);G([Hb],Vb);K=x[H(Cb)];var M=K.indexOf(Wb);if(M!=-1){y=Number(K.substring(M+1));K=K.substring(0,M)}L=K+Xb}catch(a){return}}var N;function O(){if(!s){s=true;if(!__gwt_stylesLoaded[Yb]){var a=n.createElement(Zb);__gwt_stylesLoaded[Yb]=a;a.setAttribute($b,_b);a.setAttribute(ac,t+Yb);n.getElementsByTagName(bc)[0].appendChild(a)}C();if(n.removeEventListener){n.removeEventListener(cc,O,false)}if(N){clearInterval(N)}}}
if(n.addEventListener){n.addEventListener(cc,function(){J();O()},false)}var N=setInterval(function(){if(/loaded|complete/.test(n.readyState)){J();O()}},50);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Y});o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:T});n.write(dc)} if(n.addEventListener){n.addEventListener(cc,function(){J();O()},false)}var N=setInterval(function(){if(/loaded|complete/.test(n.readyState)){J();O()}},50);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Y});o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:T});n.write(dc)}
genex(); genex();
\ No newline at end of file
...@@ -67,7 +67,7 @@ If if you aren't changing static files, can run `rake test` once, then run ...@@ -67,7 +67,7 @@ If if you aren't changing static files, can run `rake test` once, then run
or or
rake fasttest_cms rake fasttest_cms
xmodule can be tested independently, with this: xmodule can be tested independently, with this:
rake test_common/lib/xmodule rake test_common/lib/xmodule
...@@ -90,7 +90,7 @@ To run a single nose test: ...@@ -90,7 +90,7 @@ To run a single nose test:
nosetests common/lib/xmodule/xmodule/tests/test_stringify.py:test_stringify nosetests common/lib/xmodule/xmodule/tests/test_stringify.py:test_stringify
Very handy: if you uncomment the `--pdb` argument in `NOSE_ARGS` in `lms/envs/test.py`, it will drop you into pdb on error. This lets you go up and down the stack and see what the values of the variables are. Check out http://docs.python.org/library/pdb.html Very handy: if you uncomment the `pdb=1` line in `setup.cfg`, it will drop you into pdb on error. This lets you go up and down the stack and see what the values of the variables are. Check out http://docs.python.org/library/pdb.html
### Javascript Tests ### Javascript Tests
...@@ -105,7 +105,7 @@ To run the tests headless, you must install phantomjs (http://phantomjs.org/down ...@@ -105,7 +105,7 @@ To run the tests headless, you must install phantomjs (http://phantomjs.org/down
rake phantomjs_jasmine_{lms,cms} rake phantomjs_jasmine_{lms,cms}
If the `phantomjs` binary is not on the path, set the `PHANTOMJS_PATH` environment variable to point to it If the `phantomjs` binary is not on the path, set the `PHANTOMJS_PATH` environment variable to point to it
PHANTOMJS_PATH=/path/to/phantomjs rake phantomjs_jasmine_{lms,cms} PHANTOMJS_PATH=/path/to/phantomjs rake phantomjs_jasmine_{lms,cms}
...@@ -126,7 +126,7 @@ When you connect to the LMS, you need to use the public ip. Use `ifconfig` to f ...@@ -126,7 +126,7 @@ When you connect to the LMS, you need to use the public ip. Use `ifconfig` to f
## Content development ## Content development
If you change course content, while running the LMS in dev mode, it is unnecessary to restart to refresh the modulestore. If you change course content, while running the LMS in dev mode, it is unnecessary to restart to refresh the modulestore.
Instead, hit /migrate/modules to see a list of all modules loaded, and click on links (eg /migrate/reload/edx4edx) to reload a course. Instead, hit /migrate/modules to see a list of all modules loaded, and click on links (eg /migrate/reload/edx4edx) to reload a course.
......
...@@ -359,6 +359,8 @@ Supported fields at the course level ...@@ -359,6 +359,8 @@ Supported fields at the course level
* `auto_cohort_groups`: `["group name 1", "group name 2", ...]` If `cohorted` and `auto_cohort` is true, automatically put each student into a random group from the `auto_cohort_groups` list, creating the group if needed. * `auto_cohort_groups`: `["group name 1", "group name 2", ...]` If `cohorted` and `auto_cohort` is true, automatically put each student into a random group from the `auto_cohort_groups` list, creating the group if needed.
* - `pdf_textbooks` * - `pdf_textbooks`
- have pdf-based textbooks on tabs in the courseware. See below for details on config. - have pdf-based textbooks on tabs in the courseware. See below for details on config.
* - `html_textbooks`
- have html-based textbooks on tabs in the courseware. See below for details on config.
Available metadata Available metadata
...@@ -511,6 +513,7 @@ If you want to customize the courseware tabs displayed for your course, specify ...@@ -511,6 +513,7 @@ If you want to customize the courseware tabs displayed for your course, specify
"name": "Exciting news" "name": "Exciting news"
}, },
{"type": "textbooks"}, {"type": "textbooks"},
{"type": "html_textbooks"},
{"type": "pdf_textbooks"} {"type": "pdf_textbooks"}
] ]
...@@ -518,6 +521,7 @@ If you want to customize the courseware tabs displayed for your course, specify ...@@ -518,6 +521,7 @@ If you want to customize the courseware tabs displayed for your course, specify
* The first two tabs must have types `"courseware"` and `"course_info"`, in that order, or the course will not load. * The first two tabs must have types `"courseware"` and `"course_info"`, in that order, or the course will not load.
* The `courseware` tab never has a name attribute -- it's always rendered as "Courseware" for consistency between courses. * The `courseware` tab never has a name attribute -- it's always rendered as "Courseware" for consistency between courses.
* The `textbooks` tab will actually generate one tab per textbook, using the textbook titles as names. * The `textbooks` tab will actually generate one tab per textbook, using the textbook titles as names.
* The `html_textbooks` tab will actually generate one tab per html_textbook. The tab name is found in the html textbook definition.
* The `pdf_textbooks` tab will actually generate one tab per pdf_textbook. The tab name is found in the pdf textbook definition. * The `pdf_textbooks` tab will actually generate one tab per pdf_textbook. The tab name is found in the pdf textbook definition.
* For static tabs, the `url_slug` will be the url that points to the tab. It can not be one of the existing courseware url types (even if those aren't used in your course). The static content will come from `tabs/{course_url_name}/{url_slug}.html`, or `tabs/{url_slug}.html` if that doesn't exist. * For static tabs, the `url_slug` will be the url that points to the tab. It can not be one of the existing courseware url types (even if those aren't used in your course). The static content will come from `tabs/{course_url_name}/{url_slug}.html`, or `tabs/{url_slug}.html` if that doesn't exist.
* An Instructor tab will be automatically added at the end for course staff users. * An Instructor tab will be automatically added at the end for course staff users.
...@@ -538,6 +542,8 @@ If you want to customize the courseware tabs displayed for your course, specify ...@@ -538,6 +542,8 @@ If you want to customize the courseware tabs displayed for your course, specify
- Parameters `name`, `link`. - Parameters `name`, `link`.
* - `textbooks` * - `textbooks`
- No parameters--generates tab names from book titles. - No parameters--generates tab names from book titles.
* - `html_textbooks`
- No parameters--generates tab names from html book definition. (See discussion below for configuration.)
* - `pdf_textbooks` * - `pdf_textbooks`
- No parameters--generates tab names from pdf book definition. (See discussion below for configuration.) - No parameters--generates tab names from pdf book definition. (See discussion below for configuration.)
* - `progress` * - `progress`
...@@ -550,7 +556,7 @@ If you want to customize the courseware tabs displayed for your course, specify ...@@ -550,7 +556,7 @@ If you want to customize the courseware tabs displayed for your course, specify
********* *********
Textbooks Textbooks
********* *********
Support is currently provided for image-based and PDF-based textbooks. In addition to enabling the display of textbooks in tabs (see above), specific information about the location of textbook content must be configured. Support is currently provided for image-based, HTML-based and PDF-based textbooks. In addition to enabling the display of textbooks in tabs (see above), specific information about the location of textbook content must be configured.
Image-based Textbooks Image-based Textbooks
===================== =====================
...@@ -623,6 +629,62 @@ The course content can then link to page 25 using the `customtag` element: ...@@ -623,6 +629,62 @@ The course content can then link to page 25 using the `customtag` element:
<customtag book="0" page="25" impl="book"/> <customtag book="0" page="25" impl="book"/>
HTML-based Textbooks
====================
Configuration
-------------
HTML-based textbooks are configured at the course level in the policy file. The JSON markup consists of an array of maps, with each map corresponding to a separate textbook. There are two styles to presenting HTML-based material. The first way is as a single HTML on a tab, which requires only a tab title and a URL for configuration. A second way permits the display of multiple HTML files that should be displayed together on a single view. For this view, a side panel of links is available on the left, allowing selection of a particular HTML to view.
.. code-block:: json
"html_textbooks": [
{"tab_title": "Textbook 1",
"url": "https://www.example.com/thiscourse/book1/book1.html" },
{"tab_title": "Textbook 2",
"chapters": [
{ "title": "Chapter 1", "url": "https://www.example.com/thiscourse/book2/Chapter1.html" },
{ "title": "Chapter 2", "url": "https://www.example.com/thiscourse/book2/Chapter2.html" },
{ "title": "Chapter 3", "url": "https://www.example.com/thiscourse/book2/Chapter3.html" },
{ "title": "Chapter 4", "url": "https://www.example.com/thiscourse/book2/Chapter4.html" },
{ "title": "Chapter 5", "url": "https://www.example.com/thiscourse/book2/Chapter5.html" },
{ "title": "Chapter 6", "url": "https://www.example.com/thiscourse/book2/Chapter6.html" },
{ "title": "Chapter 7", "url": "https://www.example.com/thiscourse/book2/Chapter7.html" }
]
}
]
Some notes:
* It is not a good idea to include a top-level URL and chapter-level URLs in the same textbook configuration.
Linking from Content
--------------------
It is possible to add links to specific pages in a textbook by using a URL that encodes the index of the textbook, the chapter (if chapters are used), and the page number. For a book with no chapters, the URL is of the form `/course/htmlbook/${bookindex}`. For a book with chapters, use `/course/htmlbook/${bookindex}/chapter/${chapter}` for a specific chapter, or `/course/htmlbook/${bookindex}` will default to the first chapter.
For example, for the book with no chapters configured above, the textbook can be reached using the URL `/course/htmlbook/0`. Reaching the third chapter of the second book is accomplished with `/course/htmlbook/1/chapter/3`.
You can use a `customtag` to create a template for such links. For example, you can create a `htmlbook` template in the `customtag` directory, containing:
.. code-block:: xml
<img src="/static/images/icons/textbook_icon.png"/> More information given in <a href="/course/htmlbook/${book}">the text</a>.
And a `htmlchapter` template containing:
.. code-block:: xml
<img src="/static/images/icons/textbook_icon.png"/> More information given in <a href="/course/htmlbook/${book}/chapter/${chapter}">the text</a>.
The example pages can then be linked using the `customtag` element:
.. code-block:: xml
<customtag book="0" impl="htmlbook"/>
<customtag book="1" chapter="3" impl="htmlchapter"/>
PDF-based Textbooks PDF-based Textbooks
=================== ===================
......
...@@ -36,7 +36,7 @@ export PIP_DOWNLOAD_CACHE=/mnt/pip-cache ...@@ -36,7 +36,7 @@ export PIP_DOWNLOAD_CACHE=/mnt/pip-cache
source /mnt/virtualenvs/"$JOB_NAME"/bin/activate source /mnt/virtualenvs/"$JOB_NAME"/bin/activate
pip install -q -r pre-requirements.txt pip install -q -r pre-requirements.txt
yes w | pip install -q -r test-requirements.txt -r requirements.txt yes w | pip install -q -r requirements.txt
rake clobber rake clobber
rake pep8 > pep8.log || cat pep8.log rake pep8 > pep8.log || cat pep8.log
......
#pylint: disable=C0111 #pylint: disable=C0111
#pylint: disable=W0621 #pylint: disable=W0621
from __future__ import absolute_import
from lettuce import world, step from lettuce import world, step
from nose.tools import assert_equals, assert_in from nose.tools import assert_equals, assert_in
from lettuce.django import django_url from lettuce.django import django_url
......
...@@ -15,6 +15,7 @@ Feature: Answer problems ...@@ -15,6 +15,7 @@ Feature: Answer problems
| drop down | | drop down |
| multiple choice | | multiple choice |
| checkbox | | checkbox |
| radio |
| string | | string |
| numerical | | numerical |
| formula | | formula |
...@@ -33,6 +34,7 @@ Feature: Answer problems ...@@ -33,6 +34,7 @@ Feature: Answer problems
| drop down | | drop down |
| multiple choice | | multiple choice |
| checkbox | | checkbox |
| radio |
| string | | string |
| numerical | | numerical |
| formula | | formula |
...@@ -50,6 +52,7 @@ Feature: Answer problems ...@@ -50,6 +52,7 @@ Feature: Answer problems
| drop down | | drop down |
| multiple choice | | multiple choice |
| checkbox | | checkbox |
| radio |
| string | | string |
| numerical | | numerical |
| formula | | formula |
...@@ -71,6 +74,8 @@ Feature: Answer problems ...@@ -71,6 +74,8 @@ Feature: Answer problems
| multiple choice | incorrect | | multiple choice | incorrect |
| checkbox | correct | | checkbox | correct |
| checkbox | incorrect | | checkbox | incorrect |
| radio | correct |
| radio | incorrect |
| string | correct | | string | correct |
| string | incorrect | | string | incorrect |
| numerical | correct | | numerical | correct |
......
...@@ -42,7 +42,13 @@ PROBLEM_FACTORY_DICT = { ...@@ -42,7 +42,13 @@ PROBLEM_FACTORY_DICT = {
'choice_type': 'checkbox', 'choice_type': 'checkbox',
'choices': [True, False, True, False, False], 'choices': [True, False, True, False, False],
'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}}, 'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}},
'radio': {
'factory': ChoiceResponseXMLFactory(),
'kwargs': {
'question_text': 'The correct answer is Choice 3',
'choice_type': 'radio',
'choices': [False, False, True, False],
'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}},
'string': { 'string': {
'factory': StringResponseXMLFactory(), 'factory': StringResponseXMLFactory(),
'kwargs': { 'kwargs': {
...@@ -174,6 +180,12 @@ def answer_problem(step, problem_type, correctness): ...@@ -174,6 +180,12 @@ def answer_problem(step, problem_type, correctness):
else: else:
inputfield('checkbox', choice='choice_3').check() inputfield('checkbox', choice='choice_3').check()
elif problem_type == 'radio':
if correctness == 'correct':
inputfield('radio', choice='choice_2').check()
else:
inputfield('radio', choice='choice_1').check()
elif problem_type == 'string': elif problem_type == 'string':
textvalue = 'correct string' if correctness == 'correct' \ textvalue = 'correct string' if correctness == 'correct' \
else 'incorrect' else 'incorrect'
...@@ -252,6 +264,14 @@ def assert_problem_has_answer(step, problem_type, answer_class): ...@@ -252,6 +264,14 @@ def assert_problem_has_answer(step, problem_type, answer_class):
else: else:
assert_checked('checkbox', []) assert_checked('checkbox', [])
elif problem_type == "radio":
if answer_class == 'correct':
assert_checked('radio', ['choice_2'])
elif answer_class == 'incorrect':
assert_checked('radio', ['choice_1'])
else:
assert_checked('radio', [])
elif problem_type == 'string': elif problem_type == 'string':
if answer_class == 'blank': if answer_class == 'blank':
expected = '' expected = ''
...@@ -298,6 +318,7 @@ CORRECTNESS_SELECTORS = { ...@@ -298,6 +318,7 @@ CORRECTNESS_SELECTORS = {
'correct': {'drop down': ['span.correct'], 'correct': {'drop down': ['span.correct'],
'multiple choice': ['label.choicegroup_correct'], 'multiple choice': ['label.choicegroup_correct'],
'checkbox': ['span.correct'], 'checkbox': ['span.correct'],
'radio': ['label.choicegroup_correct'],
'string': ['div.correct'], 'string': ['div.correct'],
'numerical': ['div.correct'], 'numerical': ['div.correct'],
'formula': ['div.correct'], 'formula': ['div.correct'],
...@@ -308,6 +329,8 @@ CORRECTNESS_SELECTORS = { ...@@ -308,6 +329,8 @@ CORRECTNESS_SELECTORS = {
'multiple choice': ['label.choicegroup_incorrect', 'multiple choice': ['label.choicegroup_incorrect',
'span.incorrect'], 'span.incorrect'],
'checkbox': ['span.incorrect'], 'checkbox': ['span.incorrect'],
'radio': ['label.choicegroup_incorrect',
'span.incorrect'],
'string': ['div.incorrect'], 'string': ['div.incorrect'],
'numerical': ['div.incorrect'], 'numerical': ['div.incorrect'],
'formula': ['div.incorrect'], 'formula': ['div.incorrect'],
...@@ -317,6 +340,7 @@ CORRECTNESS_SELECTORS = { ...@@ -317,6 +340,7 @@ CORRECTNESS_SELECTORS = {
'unanswered': {'drop down': ['span.unanswered'], 'unanswered': {'drop down': ['span.unanswered'],
'multiple choice': ['span.unanswered'], 'multiple choice': ['span.unanswered'],
'checkbox': ['span.unanswered'], 'checkbox': ['span.unanswered'],
'radio': ['span.unanswered'],
'string': ['div.unanswered'], 'string': ['div.unanswered'],
'numerical': ['div.unanswered'], 'numerical': ['div.unanswered'],
'formula': ['div.unanswered'], 'formula': ['div.unanswered'],
......
import factory
from student.models import (User, UserProfile, Registration,
CourseEnrollmentAllowed)
from django.contrib.auth.models import Group
from datetime import datetime from datetime import datetime
import uuid import json
from functools import partial
from factory import DjangoModelFactory, SubFactory
from student.tests.factories import UserFactory as StudentUserFactory
from student.tests.factories import GroupFactory as StudentGroupFactory
from student.tests.factories import UserProfileFactory as StudentUserProfileFactory
from student.tests.factories import CourseEnrollmentAllowedFactory as StudentCourseEnrollmentAllowedFactory
from student.tests.factories import RegistrationFactory as StudentRegistrationFactory
from courseware.models import StudentModule, XModuleContentField, XModuleSettingsField
from courseware.models import XModuleStudentInfoField, XModuleStudentPrefsField
class UserProfileFactory(factory.Factory): from xmodule.modulestore import Location
FACTORY_FOR = UserProfile
user = None location = partial(Location, 'i4x', 'edX', 'test_course', 'problem')
name = 'Robot Studio'
courseware = 'course.xml'
class RegistrationFactory(factory.Factory): class UserProfileFactory(StudentUserProfileFactory):
FACTORY_FOR = Registration name = 'Robot Studio'
courseware = 'course.xml'
user = None
activation_key = uuid.uuid4().hex
class RegistrationFactory(StudentRegistrationFactory):
pass
class UserFactory(factory.Factory):
FACTORY_FOR = User
username = 'robot' class UserFactory(StudentUserFactory):
email = 'robot@edx.org' email = 'robot@edx.org'
password = 'test'
first_name = 'Robot'
last_name = 'Tester' last_name = 'Tester'
is_staff = False
is_active = True
is_superuser = False
last_login = datetime.now() last_login = datetime.now()
date_joined = datetime.now() date_joined = datetime.now()
class GroupFactory(factory.Factory): class GroupFactory(StudentGroupFactory):
FACTORY_FOR = Group
name = 'test_group' name = 'test_group'
class CourseEnrollmentAllowedFactory(factory.Factory): class CourseEnrollmentAllowedFactory(StudentCourseEnrollmentAllowedFactory):
FACTORY_FOR = CourseEnrollmentAllowed pass
class StudentModuleFactory(DjangoModelFactory):
FACTORY_FOR = StudentModule
module_type = "problem"
student = SubFactory(UserFactory)
course_id = "MITx/999/Robot_Super_Course"
state = None
grade = None
max_grade = None
done = 'na'
class ContentFactory(DjangoModelFactory):
FACTORY_FOR = XModuleContentField
field_name = 'existing_field'
value = json.dumps('old_value')
definition_id = location('def_id').url()
class SettingsFactory(DjangoModelFactory):
FACTORY_FOR = XModuleSettingsField
field_name = 'existing_field'
value = json.dumps('old_value')
usage_id = '%s-%s' % ('edX/test_course/test', location('def_id').url())
class StudentPrefsFactory(DjangoModelFactory):
FACTORY_FOR = XModuleStudentPrefsField
field_name = 'existing_field'
value = json.dumps('old_value')
student = SubFactory(UserFactory)
module_type = 'problem'
class StudentInfoFactory(DjangoModelFactory):
FACTORY_FOR = XModuleStudentInfoField
email = 'test@edx.org' field_name = 'existing_field'
course_id = 'edX/test/2012_Fall' value = json.dumps('old_value')
student = SubFactory(UserFactory)
import factory
import json import json
from mock import Mock from mock import Mock
from django.contrib.auth.models import User
from functools import partial from functools import partial
from courseware.model_data import LmsKeyValueStore, InvalidWriteError, InvalidScopeError, ModelDataCache from courseware.model_data import LmsKeyValueStore, InvalidWriteError
from courseware.models import StudentModule, XModuleContentField, XModuleSettingsField, XModuleStudentInfoField, XModuleStudentPrefsField from courseware.model_data import InvalidScopeError, ModelDataCache
from courseware.models import StudentModule, XModuleContentField, XModuleSettingsField
from courseware.models import XModuleStudentInfoField, XModuleStudentPrefsField
from student.tests.factories import UserFactory
from courseware.tests.factories import StudentModuleFactory as cmfStudentModuleFactory
from courseware.tests.factories import ContentFactory, SettingsFactory
from courseware.tests.factories import StudentPrefsFactory, StudentInfoFactory
from xblock.core import Scope, BlockScope from xblock.core import Scope, BlockScope
from xmodule.modulestore import Location from xmodule.modulestore import Location
from django.test import TestCase from django.test import TestCase
...@@ -19,6 +23,7 @@ def mock_field(scope, name): ...@@ -19,6 +23,7 @@ def mock_field(scope, name):
field.name = name field.name = name
return field return field
def mock_descriptor(fields=[], lms_fields=[]): def mock_descriptor(fields=[], lms_fields=[]):
descriptor = Mock() descriptor = Mock()
descriptor.stores_state = True descriptor.stores_state = True
...@@ -37,53 +42,9 @@ prefs_key = partial(LmsKeyValueStore.Key, Scope.preferences, 'user', 'problem') ...@@ -37,53 +42,9 @@ prefs_key = partial(LmsKeyValueStore.Key, Scope.preferences, 'user', 'problem')
user_info_key = partial(LmsKeyValueStore.Key, Scope.user_info, 'user', None) user_info_key = partial(LmsKeyValueStore.Key, Scope.user_info, 'user', None)
class UserFactory(factory.Factory): class StudentModuleFactory(cmfStudentModuleFactory):
FACTORY_FOR = User
username = 'user'
class StudentModuleFactory(factory.Factory):
FACTORY_FOR = StudentModule
module_type = 'problem'
module_state_key = location('def_id').url() module_state_key = location('def_id').url()
student = factory.SubFactory(UserFactory)
course_id = course_id course_id = course_id
state = None
class ContentFactory(factory.Factory):
FACTORY_FOR = XModuleContentField
field_name = 'existing_field'
value = json.dumps('old_value')
definition_id = location('def_id').url()
class SettingsFactory(factory.Factory):
FACTORY_FOR = XModuleSettingsField
field_name = 'existing_field'
value = json.dumps('old_value')
usage_id = '%s-%s' % (course_id, location('def_id').url())
class StudentPrefsFactory(factory.Factory):
FACTORY_FOR = XModuleStudentPrefsField
field_name = 'existing_field'
value = json.dumps('old_value')
student = factory.SubFactory(UserFactory)
module_type = 'problem'
class StudentInfoFactory(factory.Factory):
FACTORY_FOR = XModuleStudentInfoField
field_name = 'existing_field'
value = json.dumps('old_value')
student = factory.SubFactory(UserFactory)
class TestDescriptorFallback(TestCase): class TestDescriptorFallback(TestCase):
...@@ -114,7 +75,7 @@ class TestDescriptorFallback(TestCase): ...@@ -114,7 +75,7 @@ class TestDescriptorFallback(TestCase):
class TestInvalidScopes(TestCase): class TestInvalidScopes(TestCase):
def setUp(self): def setUp(self):
self.desc_md = {} self.desc_md = {}
self.user = UserFactory.create() self.user = UserFactory.create(username='user')
self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.user_state, 'a_field')])], course_id, self.user) self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.user_state, 'a_field')])], course_id, self.user)
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc) self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
...@@ -180,7 +141,7 @@ class TestStudentModuleStorage(TestCase): ...@@ -180,7 +141,7 @@ class TestStudentModuleStorage(TestCase):
class TestMissingStudentModule(TestCase): class TestMissingStudentModule(TestCase):
def setUp(self): def setUp(self):
self.user = UserFactory.create() self.user = UserFactory.create(username='user')
self.desc_md = {} self.desc_md = {}
self.mdc = ModelDataCache([mock_descriptor()], course_id, self.user) self.mdc = ModelDataCache([mock_descriptor()], course_id, self.user)
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc) self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
......
...@@ -55,7 +55,7 @@ def mongo_store_config(data_dir): ...@@ -55,7 +55,7 @@ def mongo_store_config(data_dir):
Use of this config requires mongo to be running Use of this config requires mongo to be running
''' '''
return { store = {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': { 'OPTIONS': {
...@@ -68,6 +68,8 @@ def mongo_store_config(data_dir): ...@@ -68,6 +68,8 @@ def mongo_store_config(data_dir):
} }
} }
} }
store['direct'] = store['default']
return store
def draft_mongo_store_config(data_dir): def draft_mongo_store_config(data_dir):
...@@ -83,6 +85,17 @@ def draft_mongo_store_config(data_dir): ...@@ -83,6 +85,17 @@ def draft_mongo_store_config(data_dir):
'fs_root': data_dir, 'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string', 'render_template': 'mitxmako.shortcuts.render_to_string',
} }
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore',
'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string',
}
} }
} }
......
...@@ -38,7 +38,7 @@ class Role(models.Model): ...@@ -38,7 +38,7 @@ class Role(models.Model):
def inherit_permissions(self, role): # TODO the name of this method is a little bit confusing, def inherit_permissions(self, role): # TODO the name of this method is a little bit confusing,
# since it's one-off and doesn't handle inheritance later # since it's one-off and doesn't handle inheritance later
if role.course_id and role.course_id != self.course_id: if role.course_id and role.course_id != self.course_id:
logging.warning("{0} cannot inherit permissions from {1} due to course_id inconsistency", \ logging.warning("%s cannot inherit permissions from %s due to course_id inconsistency", \
self, role) self, role)
for per in role.permissions.all(): for per in role.permissions.all():
self.add_permission(per) self.add_permission(per)
......
import string
import random
import collections
from django.test import TestCase from django.test import TestCase
from factory import DjangoModelFactory
from student.tests.factories import UserFactory, CourseEnrollmentFactory
import factory
from django.contrib.auth.models import User
from student.models import UserProfile, CourseEnrollment
from django_comment_client.models import Role, Permission from django_comment_client.models import Role, Permission
import django_comment_client.models as models
import django_comment_client.utils as utils import django_comment_client.utils as utils
import xmodule.modulestore.django as django
class UserFactory(factory.Factory):
FACTORY_FOR = User
username = 'robot'
password = '123456'
email = 'robot@edx.org'
is_active = True
is_staff = False
class CourseEnrollmentFactory(factory.Factory):
FACTORY_FOR = CourseEnrollment
user = factory.SubFactory(UserFactory)
course_id = 'edX/toy/2012_Fall'
class RoleFactory(factory.Factory): class RoleFactory(DjangoModelFactory):
FACTORY_FOR = Role FACTORY_FOR = Role
name = 'Student' name = 'Student'
course_id = 'edX/toy/2012_Fall' course_id = 'edX/toy/2012_Fall'
class PermissionFactory(factory.Factory): class PermissionFactory(DjangoModelFactory):
FACTORY_FOR = Permission FACTORY_FOR = Permission
name = 'create_comment' name = 'create_comment'
......
"""
Unit tests for instructor dashboard
Based on (and depends on) unit tests for courseware.
Notes for running by hand:
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/instructor
"""
from django.test.utils import override_settings
# Need access to internal func to put users in the right group
from django.contrib.auth.models import Group
from django.core.urlresolvers import reverse
from courseware.access import _course_staff_group_name
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase):
'''
Check for download of csv
'''
def setUp(self):
xmodule.modulestore.django._MODULESTORES = {}
self.full = modulestore().get_course("edX/full/6.002_Spring_2012")
self.toy = modulestore().get_course("edX/toy/2012_Fall")
# Create two accounts
self.student = 'view@test.com'
self.instructor = 'view2@test.com'
self.password = 'foo'
self.create_account('u1', self.student, self.password)
self.create_account('u2', self.instructor, self.password)
self.activate_user(self.student)
self.activate_user(self.instructor)
def make_instructor(course):
group_name = _course_staff_group_name(course.location)
g = Group.objects.create(name=group_name)
g.user_set.add(get_user(self.instructor))
make_instructor(self.toy)
self.logout()
self.login(self.instructor, self.password)
self.enroll(self.toy)
def test_download_grades_csv(self):
course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
msg = "url = {0}\n".format(url)
response = self.client.post(url, {'action': 'Download CSV of all student grades for this course'})
msg += "instructor dashboard download csv grades: response = '{0}'\n".format(response)
self.assertEqual(response['Content-Type'], 'text/csv', msg)
cdisp = response['Content-Disposition']
msg += "Content-Disposition = '%s'\n" % cdisp
self.assertEqual(cdisp, 'attachment; filename=grades_{0}.csv'.format(course.id), msg)
body = response.content.replace('\r', '')
msg += "body = '{0}'\n".format(body)
# All the not-actually-in-the-course hw and labs come from the
# default grading policy string in graders.py
expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final"
"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0"
'''
self.assertEqual(body, expected_body, msg)
""" """
Unit tests for instructor dashboard Unit tests for instructor dashboard forum administration
Based on (and depends on) unit tests for courseware.
Notes for running by hand:
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/instructor
""" """
from django.test.utils import override_settings from django.test.utils import override_settings
# Need access to internal func to put users in the right group # Need access to internal func to put users in the right group
...@@ -24,63 +19,6 @@ from xmodule.modulestore.django import modulestore ...@@ -24,63 +19,6 @@ from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django import xmodule.modulestore.django
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase):
'''
Check for download of csv
'''
def setUp(self):
xmodule.modulestore.django._MODULESTORES = {}
self.full = modulestore().get_course("edX/full/6.002_Spring_2012")
self.toy = modulestore().get_course("edX/toy/2012_Fall")
# Create two accounts
self.student = 'view@test.com'
self.instructor = 'view2@test.com'
self.password = 'foo'
self.create_account('u1', self.student, self.password)
self.create_account('u2', self.instructor, self.password)
self.activate_user(self.student)
self.activate_user(self.instructor)
def make_instructor(course):
group_name = _course_staff_group_name(course.location)
g = Group.objects.create(name=group_name)
g.user_set.add(get_user(self.instructor))
make_instructor(self.toy)
self.logout()
self.login(self.instructor, self.password)
self.enroll(self.toy)
def test_download_grades_csv(self):
course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
msg = "url = {0}\n".format(url)
response = self.client.post(url, {'action': 'Download CSV of all student grades for this course'})
msg += "instructor dashboard download csv grades: response = '{0}'\n".format(response)
self.assertEqual(response['Content-Type'], 'text/csv', msg)
cdisp = response['Content-Disposition']
msg += "Content-Disposition = '%s'\n" % cdisp
self.assertEqual(cdisp, 'attachment; filename=grades_{0}.csv'.format(course.id), msg)
body = response.content.replace('\r', '')
msg += "body = '{0}'\n".format(body)
# All the not-actually-in-the-course hw and labs come from the
# default grading policy string in graders.py
expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final"
"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0"
'''
self.assertEqual(body, expected_body, msg)
FORUM_ROLES = [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA] FORUM_ROLES = [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA]
FORUM_ADMIN_ACTION_SUFFIX = {FORUM_ROLE_ADMINISTRATOR: 'admin', FORUM_ROLE_MODERATOR: 'moderator', FORUM_ROLE_COMMUNITY_TA: 'community TA'} FORUM_ADMIN_ACTION_SUFFIX = {FORUM_ROLE_ADMINISTRATOR: 'admin', FORUM_ROLE_MODERATOR: 'moderator', FORUM_ROLE_COMMUNITY_TA: 'community TA'}
FORUM_ADMIN_USER = {FORUM_ROLE_ADMINISTRATOR: 'forumadmin', FORUM_ROLE_MODERATOR: 'forummoderator', FORUM_ROLE_COMMUNITY_TA: 'forummoderator'} FORUM_ADMIN_USER = {FORUM_ROLE_ADMINISTRATOR: 'forumadmin', FORUM_ROLE_MODERATOR: 'forummoderator', FORUM_ROLE_COMMUNITY_TA: 'forummoderator'}
...@@ -208,4 +146,4 @@ class TestInstructorDashboardForumAdmin(LoginEnrollmentTestCase): ...@@ -208,4 +146,4 @@ class TestInstructorDashboardForumAdmin(LoginEnrollmentTestCase):
added_roles.append(rolename) added_roles.append(rolename)
added_roles.sort() added_roles.sort()
roles = ', '.join(added_roles) roles = ', '.join(added_roles)
self.assertTrue(response.content.find('<td>{0}</td>'.format(roles)) >= 0, 'not finding roles "{0}"'.format(roles)) self.assertTrue(response.content.find('<td>{0}</td>'.format(roles)) >= 0, 'not finding roles "{0}"'.format(roles))
\ No newline at end of file
"""
Tests of the instructor dashboard gradebook
"""
from django.test import TestCase
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from student.tests.factories import UserFactory, CourseEnrollmentFactory, UserProfileFactory, AdminFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from mock import patch, DEFAULT
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from capa.tests.response_xml_factory import StringResponseXMLFactory
from courseware.tests.factories import StudentModuleFactory
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
USER_COUNT = 11
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestGradebook(ModuleStoreTestCase):
grading_policy = None
def setUp(self):
instructor = AdminFactory.create()
self.client.login(username=instructor.username, password='test')
modulestore().request_cache = modulestore().metadata_inheritance_cache_subsystem = None
course_data = {}
if self.grading_policy is not None:
course_data['grading_policy'] = self.grading_policy
self.course = CourseFactory.create(data=course_data)
chapter = ItemFactory.create(
parent_location=self.course.location,
template="i4x://edx/templates/sequential/Empty",
)
section = ItemFactory.create(
parent_location=chapter.location,
template="i4x://edx/templates/sequential/Empty",
metadata={'graded': True, 'format': 'Homework'}
)
self.users = [
UserFactory.create(username='robot%d' % i, email='robot+test+%d@edx.org' % i)
for i in xrange(USER_COUNT)
]
for user in self.users:
UserProfileFactory.create(user=user)
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
for i in xrange(USER_COUNT-1):
template_name = "i4x://edx/templates/problem/Blank_Common_Problem"
item = ItemFactory.create(
parent_location=section.location,
template=template_name,
data=StringResponseXMLFactory().build_xml(answer='foo'),
metadata={'rerandomize': 'always'}
)
for j, user in enumerate(self.users):
StudentModuleFactory.create(
grade=1 if i < j else 0,
max_grade=1,
student=user,
course_id=self.course.id,
module_state_key=Location(item.location).url()
)
self.response = self.client.get(reverse('gradebook', args=(self.course.id,)))
def test_response_code(self):
self.assertEquals(self.response.status_code, 200)
class TestDefaultGradingPolicy(TestGradebook):
def test_all_users_listed(self):
for user in self.users:
self.assertIn(user.username, self.response.content)
def test_default_policy(self):
# Default >= 50% passes, so Users 5-10 should be passing for Homework 1 [6]
# One use at the top of the page [1]
self.assertEquals(7, self.response.content.count('grade_Pass'))
# Users 1-5 attempted Homework 1 (and get Fs) [4]
# Users 1-10 attempted any homework (and get Fs) [10]
# Users 4-10 scored enough to not get rounded to 0 for the class (and get Fs) [7]
# One use at top of the page [1]
self.assertEquals(22, self.response.content.count('grade_F'))
# All other grades are None [29 categories * 11 users - 27 non-empty grades = 292]
# One use at the top of the page [1]
self.assertEquals(293, self.response.content.count('grade_None'))
class TestLetterCutoffPolicy(TestGradebook):
grading_policy = {
"GRADER": [
{
"type": "Homework",
"min_count": 1,
"drop_count": 0,
"short_label": "HW",
"weight": 1
},
],
"GRADE_CUTOFFS": {
'A': .9,
'B': .8,
'C': .7,
'D': .6,
}
}
def test_styles(self):
self.assertIn("grade_A {color:green;}", self.response.content)
self.assertIn("grade_B {color:Chocolate;}", self.response.content)
self.assertIn("grade_C {color:DarkSlateGray;}", self.response.content)
self.assertIn("grade_D {color:DarkSlateGray;}", self.response.content)
def test_assigned_grades(self):
print self.response.content
# Users 9-10 have >= 90% on Homeworks [2]
# Users 9-10 have >= 90% on the class [2]
# One use at the top of the page [1]
self.assertEquals(5, self.response.content.count('grade_A'))
# User 8 has 80 <= Homeworks < 90 [1]
# User 8 has 80 <= class < 90 [1]
# One use at the top of the page [1]
self.assertEquals(3, self.response.content.count('grade_B'))
# User 7 has 70 <= Homeworks < 80 [1]
# User 7 has 70 <= class < 80 [1]
# One use at the top of the page [1]
self.assertEquals(3, self.response.content.count('grade_C'))
# User 6 has 60 <= Homeworks < 70 [1]
# User 6 has 60 <= class < 70 [1]
# One use at the top of the page [1]
self.assertEquals(3, self.response.content.count('grade_C'))
# Users 1-5 have 60% > grades > 0 on Homeworks [5]
# Users 1-5 have 60% > grades > 0 on the class [5]
# One use at top of the page [1]
self.assertEquals(11, self.response.content.count('grade_F'))
# User 0 has 0 on Homeworks [1]
# User 0 has 0 on the class [1]
# One use at the top of the page [1]
self.assertEquals(3, self.response.content.count('grade_None'))
\ No newline at end of file
...@@ -961,11 +961,14 @@ def gradebook(request, course_id): ...@@ -961,11 +961,14 @@ def gradebook(request, course_id):
} }
for student in enrolled_students] for student in enrolled_students]
return render_to_response('courseware/gradebook.html', {'students': student_info, return render_to_response('courseware/gradebook.html', {
'course': course, 'students': student_info,
'course_id': course_id, 'course': course,
# Checked above 'course_id': course_id,
'staff_access': True, }) # Checked above
'staff_access': True,
'ordered_grades': sorted(course.grade_cutoffs.items(), key=lambda i: i[1], reverse=True),
})
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
......
...@@ -73,7 +73,7 @@ def _create_license(user, software): ...@@ -73,7 +73,7 @@ def _create_license(user, software):
license.save() license.save()
except IndexError: except IndexError:
# there are no free licenses # there are no free licenses
log.error('No serial numbers available for {0}', software) log.error('No serial numbers available for %s', software)
license = None license = None
# TODO [rocha]look if someone has unenrolled from the class # TODO [rocha]look if someone has unenrolled from the class
# and already has a serial number # and already has a serial number
......
...@@ -5,13 +5,21 @@ import json ...@@ -5,13 +5,21 @@ import json
from uuid import uuid4 from uuid import uuid4
from random import shuffle from random import shuffle
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from factory import Factory, SubFactory from factory import DjangoModelFactory, SubFactory
from django.test import TestCase from django.test import TestCase
from django.test.client import Client
from django.test.utils import override_settings
from django.core.management import call_command from django.core.management import call_command
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from nose.tools import assert_true
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from licenses.models import CourseSoftware, UserLicense from licenses.models import CourseSoftware, UserLicense
from courseware.tests.tests import LoginEnrollmentTestCase, get_user
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
COURSE_1 = 'edX/toy/2012_Fall' COURSE_1 = 'edX/toy/2012_Fall'
...@@ -23,7 +31,7 @@ SERIAL_1 = '123456abcde' ...@@ -23,7 +31,7 @@ SERIAL_1 = '123456abcde'
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class CourseSoftwareFactory(Factory): class CourseSoftwareFactory(DjangoModelFactory):
'''Factory for generating CourseSoftware objects in database''' '''Factory for generating CourseSoftware objects in database'''
FACTORY_FOR = CourseSoftware FACTORY_FOR = CourseSoftware
...@@ -33,7 +41,7 @@ class CourseSoftwareFactory(Factory): ...@@ -33,7 +41,7 @@ class CourseSoftwareFactory(Factory):
course_id = COURSE_1 course_id = COURSE_1
class UserLicenseFactory(Factory): class UserLicenseFactory(DjangoModelFactory):
''' '''
Factory for generating UserLicense objects in database Factory for generating UserLicense objects in database
...@@ -42,19 +50,24 @@ class UserLicenseFactory(Factory): ...@@ -42,19 +50,24 @@ class UserLicenseFactory(Factory):
''' '''
FACTORY_FOR = UserLicense FACTORY_FOR = UserLicense
user = None
software = SubFactory(CourseSoftwareFactory) software = SubFactory(CourseSoftwareFactory)
serial = SERIAL_1 serial = SERIAL_1
class LicenseTestCase(LoginEnrollmentTestCase): class LicenseTestCase(TestCase):
'''Tests for licenses.views''' '''Tests for licenses.views'''
def setUp(self): def setUp(self):
'''creates a user and logs in''' '''creates a user and logs in'''
self.setup_viewtest_user() # self.setup_viewtest_user()
self.user = UserFactory(username='test',
email='test@edx.org', password='test_password')
self.client = Client()
assert_true(self.client.login(username='test', password='test_password'))
self.software = CourseSoftwareFactory() self.software = CourseSoftwareFactory()
def test_get_license(self): def test_get_license(self):
UserLicenseFactory(user=get_user(self.viewtest_email), software=self.software) UserLicenseFactory(user=self.user, software=self.software)
response = self.client.post(reverse('user_software_license'), response = self.client.post(reverse('user_software_license'),
{'software': SOFTWARE_1, 'generate': 'false'}, {'software': SOFTWARE_1, 'generate': 'false'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest', HTTP_X_REQUESTED_WITH='XMLHttpRequest',
...@@ -121,7 +134,7 @@ class LicenseTestCase(LoginEnrollmentTestCase): ...@@ -121,7 +134,7 @@ class LicenseTestCase(LoginEnrollmentTestCase):
self.assertEqual(404, response.status_code) self.assertEqual(404, response.status_code)
def test_get_license_without_login(self): def test_get_license_without_login(self):
self.logout() self.client.logout()
response = self.client.post(reverse('user_software_license'), response = self.client.post(reverse('user_software_license'),
{'software': SOFTWARE_1, 'generate': 'false'}, {'software': SOFTWARE_1, 'generate': 'false'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest', HTTP_X_REQUESTED_WITH='XMLHttpRequest',
...@@ -130,20 +143,24 @@ class LicenseTestCase(LoginEnrollmentTestCase): ...@@ -130,20 +143,24 @@ class LicenseTestCase(LoginEnrollmentTestCase):
self.assertEqual(302, response.status_code) self.assertEqual(302, response.status_code)
class CommandTest(TestCase): @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class CommandTest(ModuleStoreTestCase):
'''Test management command for importing serial numbers''' '''Test management command for importing serial numbers'''
def setUp(self):
course = CourseFactory.create()
self.course_id = course.id
def test_import_serial_numbers(self): def test_import_serial_numbers(self):
size = 20 size = 20
log.debug('Adding one set of serials for {0}'.format(SOFTWARE_1)) log.debug('Adding one set of serials for {0}'.format(SOFTWARE_1))
with generate_serials_file(size) as temp_file: with generate_serials_file(size) as temp_file:
args = [COURSE_1, SOFTWARE_1, temp_file.name] args = [self.course_id, SOFTWARE_1, temp_file.name]
call_command('import_serial_numbers', *args) call_command('import_serial_numbers', *args)
log.debug('Adding one set of serials for {0}'.format(SOFTWARE_2)) log.debug('Adding one set of serials for {0}'.format(SOFTWARE_2))
with generate_serials_file(size) as temp_file: with generate_serials_file(size) as temp_file:
args = [COURSE_1, SOFTWARE_2, temp_file.name] args = [self.course_id, SOFTWARE_2, temp_file.name]
call_command('import_serial_numbers', *args) call_command('import_serial_numbers', *args)
log.debug('There should be only 2 course-software entries') log.debug('There should be only 2 course-software entries')
...@@ -156,7 +173,7 @@ class CommandTest(TestCase): ...@@ -156,7 +173,7 @@ class CommandTest(TestCase):
log.debug('Adding more serial numbers to {0}'.format(SOFTWARE_1)) log.debug('Adding more serial numbers to {0}'.format(SOFTWARE_1))
with generate_serials_file(size) as temp_file: with generate_serials_file(size) as temp_file:
args = [COURSE_1, SOFTWARE_1, temp_file.name] args = [self.course_id, SOFTWARE_1, temp_file.name]
call_command('import_serial_numbers', *args) call_command('import_serial_numbers', *args)
log.debug('There should be still only 2 course-software entries') log.debug('There should be still only 2 course-software entries')
...@@ -179,7 +196,7 @@ class CommandTest(TestCase): ...@@ -179,7 +196,7 @@ class CommandTest(TestCase):
with NamedTemporaryFile() as tmpfile: with NamedTemporaryFile() as tmpfile:
tmpfile.write('\n'.join(known_serials)) tmpfile.write('\n'.join(known_serials))
tmpfile.flush() tmpfile.flush()
args = [COURSE_1, SOFTWARE_1, tmpfile.name] args = [self.course_id, SOFTWARE_1, tmpfile.name]
call_command('import_serial_numbers', *args) call_command('import_serial_numbers', *args)
log.debug('Check if we added only the new ones') log.debug('Check if we added only the new ones')
......
...@@ -310,19 +310,24 @@ def save_grade(request, course_id): ...@@ -310,19 +310,24 @@ def save_grade(request, course_id):
if request.method != 'POST': if request.method != 'POST':
raise Http404 raise Http404
p = request.POST
required = set(['score', 'feedback', 'submission_id', 'location', 'submission_flagged', 'rubric_scores[]']) required = set(['score', 'feedback', 'submission_id', 'location', 'submission_flagged'])
actual = set(request.POST.keys()) skipped = 'skipped' in p
#If the instructor has skipped grading the submission, then there will not be any rubric scores.
#Only add in the rubric scores if the instructor has not skipped.
if not skipped:
required|=set(['rubric_scores[]'])
actual = set(p.keys())
missing = required - actual missing = required - actual
if len(missing) > 0: if len(missing) > 0:
return _err_response('Missing required keys {0}'.format( return _err_response('Missing required keys {0}'.format(
', '.join(missing))) ', '.join(missing)))
grader_id = unique_id_for_user(request.user) grader_id = unique_id_for_user(request.user)
p = request.POST
location = p['location'] location = p['location']
skipped = 'skipped' in p
try: try:
result_json = staff_grading_service().save_grade(course_id, result_json = staff_grading_service().save_grade(course_id,
......
...@@ -97,7 +97,7 @@ class TestStaffGradingService(LoginEnrollmentTestCase): ...@@ -97,7 +97,7 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
self.assertIsNotNone(d['rubric']) self.assertIsNotNone(d['rubric'])
def test_save_grade(self): def save_grade_base(self,skip=False):
self.login(self.instructor, self.password) self.login(self.instructor, self.password)
url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id}) url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id})
...@@ -108,12 +108,20 @@ class TestStaffGradingService(LoginEnrollmentTestCase): ...@@ -108,12 +108,20 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
'location': self.location, 'location': self.location,
'submission_flagged': "true", 'submission_flagged': "true",
'rubric_scores[]': ['1', '2']} 'rubric_scores[]': ['1', '2']}
if skip:
data.update({'skipped' : True})
r = self.check_for_post_code(200, url, data) r = self.check_for_post_code(200, url, data)
d = json.loads(r.content) d = json.loads(r.content)
self.assertTrue(d['success'], str(d)) self.assertTrue(d['success'], str(d))
self.assertEquals(d['submission_id'], self.mock_service.cnt) self.assertEquals(d['submission_id'], self.mock_service.cnt)
def test_save_grade(self):
self.save_grade_base(skip=False)
def test_save_grade_skip(self):
self.save_grade_base(skip=True)
def test_get_problem_list(self): def test_get_problem_list(self):
self.login(self.instructor, self.password) self.login(self.instructor, self.password)
......
from lxml import etree
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import Http404 from django.http import Http404
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
...@@ -38,6 +36,20 @@ def index_shifted(request, course_id, page): ...@@ -38,6 +36,20 @@ def index_shifted(request, course_id, page):
@login_required @login_required
def pdf_index(request, course_id, book_index, chapter=None, page=None): def pdf_index(request, course_id, book_index, chapter=None, page=None):
"""
Display a PDF textbook.
course_id: course for which to display text. The course should have
"pdf_textbooks" property defined.
book index: zero-based index of which PDF textbook to display.
chapter: (optional) one-based index into the chapter array of textbook PDFs to display.
Defaults to first chapter. Specifying this assumes that there are separate PDFs for
each chapter in a textbook.
page: (optional) one-based page number to display within the PDF. Defaults to first page.
"""
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff') staff_access = has_access(request.user, course, 'staff')
...@@ -63,7 +75,6 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None): ...@@ -63,7 +75,6 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None):
for entry in textbook['chapters']: for entry in textbook['chapters']:
entry['url'] = remap_static_url(entry['url'], course) entry['url'] = remap_static_url(entry['url'], course)
return render_to_response('static_pdfbook.html', return render_to_response('static_pdfbook.html',
{'book_index': book_index, {'book_index': book_index,
'course': course, 'course': course,
...@@ -72,8 +83,21 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None): ...@@ -72,8 +83,21 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None):
'page': page, 'page': page,
'staff_access': staff_access}) 'staff_access': staff_access})
@login_required @login_required
def html_index(request, course_id, book_index, chapter=None, anchor_id=None): def html_index(request, course_id, book_index, chapter=None):
"""
Display an HTML textbook.
course_id: course for which to display text. The course should have
"html_textbooks" property defined.
book index: zero-based index of which HTML textbook to display.
chapter: (optional) one-based index into the chapter array of textbook HTML files to display.
Defaults to first chapter. Specifying this assumes that there are separate HTML files for
each chapter in a textbook.
"""
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff') staff_access = has_access(request.user, course, 'staff')
...@@ -99,11 +123,9 @@ def html_index(request, course_id, book_index, chapter=None, anchor_id=None): ...@@ -99,11 +123,9 @@ def html_index(request, course_id, book_index, chapter=None, anchor_id=None):
for entry in textbook['chapters']: for entry in textbook['chapters']:
entry['url'] = remap_static_url(entry['url'], course) entry['url'] = remap_static_url(entry['url'], course)
return render_to_response('static_htmlbook.html', return render_to_response('static_htmlbook.html',
{'book_index': book_index, {'book_index': book_index,
'course': course, 'course': course,
'textbook': textbook, 'textbook': textbook,
'chapter': chapter, 'chapter': chapter,
'anchor_id': anchor_id,
'staff_access': staff_access}) 'staff_access': staff_access})
...@@ -67,3 +67,4 @@ MITX_FEATURES['STUB_VIDEO_FOR_TESTING'] = True ...@@ -67,3 +67,4 @@ MITX_FEATURES['STUB_VIDEO_FOR_TESTING'] = True
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
INSTALLED_APPS += ('lettuce.django',) INSTALLED_APPS += ('lettuce.django',)
LETTUCE_APPS = ('courseware',) LETTUCE_APPS = ('courseware',)
LETTUCE_BROWSER = 'chrome'
...@@ -27,10 +27,6 @@ SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead ...@@ -27,10 +27,6 @@ SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
# Nose Test Runner # Nose Test Runner
INSTALLED_APPS += ('django_nose',) INSTALLED_APPS += ('django_nose',)
NOSE_ARGS = [
'--with-xunit',
# '-v', '--pdb', # When really stuck, uncomment to start debugger on error
]
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
# Local Directories # Local Directories
...@@ -91,7 +87,7 @@ MODULESTORE = { ...@@ -91,7 +87,7 @@ MODULESTORE = {
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': PROJECT_ROOT / "db" / "mitx.db", 'NAME': TEST_ROOT / 'db' / 'mitx.db'
}, },
} }
...@@ -122,7 +118,7 @@ CACHES = { ...@@ -122,7 +118,7 @@ CACHES = {
'LOCATION': '/var/tmp/mongo_metadata_inheritance', 'LOCATION': '/var/tmp/mongo_metadata_inheritance',
'TIMEOUT': 300, 'TIMEOUT': 300,
'KEY_FUNCTION': 'util.memcache.safe_key', 'KEY_FUNCTION': 'util.memcache.safe_key',
} }
} }
# Dummy secret key for dev # Dummy secret key for dev
......
...@@ -185,6 +185,7 @@ class @StaffGrading ...@@ -185,6 +185,7 @@ class @StaffGrading
$(window).keydown @keydown_handler $(window).keydown @keydown_handler
$(window).keyup @keyup_handler
@question_header = $('.question-header') @question_header = $('.question-header')
@question_header.click @collapse_question @question_header.click @collapse_question
@collapse_question() @collapse_question()
...@@ -206,6 +207,7 @@ class @StaffGrading ...@@ -206,6 +207,7 @@ class @StaffGrading
@num_pending = 0 @num_pending = 0
@score_lst = [] @score_lst = []
@grade = null @grade = null
@is_ctrl = false
@problems = null @problems = null
...@@ -231,10 +233,18 @@ class @StaffGrading ...@@ -231,10 +233,18 @@ class @StaffGrading
@state = state_graded @state = state_graded
@submit_button.show() @submit_button.show()
keydown_handler: (e) => keydown_handler: (event) =>
if e.which == 13 && !@list_view && Rubric.check_complete() #Previously, responses were submitted when hitting enter. Add in a modifier that ensures that ctrl+enter is needed.
if event.which == 17 && @is_ctrl==false
@is_ctrl=true
else if @is_ctrl==true && event.which == 13 && !@list_view && Rubric.check_complete()
@submit_and_get_next() @submit_and_get_next()
keyup_handler: (event) =>
#Handle keyup event when ctrl key is released
if event.which == 17 && @is_ctrl==true
@is_ctrl=false
set_button_text: (text) => set_button_text: (text) =>
@action_button.attr('value', text) @action_button.attr('value', text)
......
...@@ -13,9 +13,12 @@ ...@@ -13,9 +13,12 @@
<%static:css group='course'/> <%static:css group='course'/>
<style type="text/css"> <style type="text/css">
.grade_A {color:green;} % for (grade, _), color in zip(ordered_grades, ['green', 'Chocolate']):
.grade_B {color:Chocolate;} .grade_${grade} {color:${color};}
.grade_C {color:DarkSlateGray;} % endfor
% for (grade, _) in ordered_grades[2:]:
.grade_${grade} {color:DarkSlateGray;}
% endfor
.grade_F {color:DimGray;} .grade_F {color:DimGray;}
.grade_None {color:LightGray;} .grade_None {color:LightGray;}
</style> </style>
...@@ -78,8 +81,8 @@ ...@@ -78,8 +81,8 @@
letter_grade = 'None' letter_grade = 'None'
if fraction > 0: if fraction > 0:
letter_grade = 'F' letter_grade = 'F'
for grade in ['A', 'B', 'C']: for (grade, cutoff) in ordered_grades:
if fraction >= course.grade_cutoffs[grade]: if fraction >= cutoff:
letter_grade = grade letter_grade = grade
break break
...@@ -90,11 +93,11 @@ ...@@ -90,11 +93,11 @@
<tbody> <tbody>
%for student in students: %for student in students:
<tr> <tr>
%for section in student['grade_summary']['section_breakdown']: %for section in student['grade_summary']['section_breakdown']:
${percent_data( section['percent'] )} ${percent_data( section['percent'] )}
%endfor %endfor
<td>${percent_data( student['grade_summary']['percent'])}</td> ${percent_data( student['grade_summary']['percent'])}
</tr> </tr>
%endfor %endfor
</tbody> </tbody>
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
<updated>2012-12-19T14:00:00-07:00</updated> <updated>2012-12-19T14:00:00-07:00</updated>
<link type="text/html" rel="alternate" href="${reverse('press/stanford-to-work-with-edx')}"/> <link type="text/html" rel="alternate" href="${reverse('press/stanford-to-work-with-edx')}"/>
<title>Stanford University to Collaborate with edX on Development of Non-Profit Open Source edX Platform</title> <title>Stanford University to Collaborate with edX on Development of Non-Profit Open Source edX Platform</title>
<content type="html">&lt;img src=&quot;${static.url('images/press/releases/stanford-university_204x114.png')}&quot; /&gt; <content type="html">&lt;img src=&quot;${static.url('images/press/releases/stanford-university-m.png')}&quot; /&gt;
&lt;p&gt;&lt;/p&gt;</content> &lt;p&gt;&lt;/p&gt;</content>
</entry> </entry>
<entry> <entry>
......
...@@ -43,8 +43,8 @@ ...@@ -43,8 +43,8 @@
<p>Please include some written feedback as well.</p> <p>Please include some written feedback as well.</p>
<textarea name="feedback" placeholder="Feedback for student" <textarea name="feedback" placeholder="Feedback for student"
class="feedback-area" cols="70" ></textarea> class="feedback-area" cols="70" ></textarea>
<div class="flag-student-container"> <input type="checkbox" class="flag-checkbox" value="student_is_flagged"> Flag this submission for review by course staff (use if the submission contains inappropriate content) </div> <div class="flag-student-container"> This submission has explicit or pornographic content : <input type="checkbox" class="flag-checkbox" value="student_is_flagged"> </div>
<div class="answer-unknown-container"> <input type="checkbox" class="answer-unknown-checkbox" value="answer_is_unknown"> I do not know how to grade this question </div> <div class="answer-unknown-container"> I do not know how to grade this question : <input type="checkbox" class="answer-unknown-checkbox" value="answer_is_unknown"></div>
</div> </div>
...@@ -82,6 +82,19 @@ ...@@ -82,6 +82,19 @@
<input type="button" class="calibration-interstitial-page-button" value="Start learning to grade" name="calibration-interstitial-page-button" /> <input type="button" class="calibration-interstitial-page-button" value="Start learning to grade" name="calibration-interstitial-page-button" />
</section> </section>
<input type="button" value="Go Back" class="action-button" name="back" /> <!-- Flag submission confirmation dialog -->
<section class="flag-submission-confirmation">
<h4> Are you sure that you want to flag this submission?</h4>
<p>
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.
</p>
<div>
<input type="button" class="flag-submission-removal-button" value="Remove Flag" name="calibration-interstitial-page-button" />
<input type="button" class="flag-submission-confirmation-button" value="Keep Flag" name="calibration-interstitial-page-button" />
</div>
</section>
<input type="button" value="Go Back" class="action-button" name="back" />
</div> </div>
</section> </section>
...@@ -26,32 +26,22 @@ ...@@ -26,32 +26,22 @@
// chapters, and it should be in-bounds. // chapters, and it should be in-bounds.
chapterToLoad = options.chapterNum; 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: // clear out previous load, if any:
parentElement = document.getElementById('bookpage'); parentElement = document.getElementById('bookpage');
while (parentElement.hasChildNodes()) while (parentElement.hasChildNodes())
parentElement.removeChild(parentElement.lastChild); parentElement.removeChild(parentElement.lastChild);
// load new URL in: // load new URL in:
$('#bookpage').load(url); $('#bookpage').load(url);
};
// if there is an anchor set, then go to that location: loadChapterUrl = function htmlViewLoadChapterUrl(chapterNum) {
if (anchorId != null) {
// TODO: add implementation....
}
};
loadChapterUrl = function htmlViewLoadChapterUrl(chapterNum, anchorId) {
if (chapterNum < 1 || chapterNum > chapterUrls.length) { if (chapterNum < 1 || chapterNum > chapterUrls.length) {
return; return;
} }
var chapterUrl = chapterUrls[chapterNum-1]; var chapterUrl = chapterUrls[chapterNum-1];
loadUrl(chapterUrl, anchorId); loadUrl(chapterUrl);
}; };
// define navigation links for chapters: // define navigation links for chapters:
...@@ -64,15 +54,15 @@ ...@@ -64,15 +54,15 @@
}; };
for (var index = 1; index <= chapterUrls.length; index += 1) { for (var index = 1; index <= chapterUrls.length; index += 1) {
$("#htmlchapter-" + index).click(loadChapterUrlHelper(index)); $("#htmlchapter-" + index).click(loadChapterUrlHelper(index));
} }
} }
// finally, load the appropriate url/page // finally, load the appropriate url/page
if (urlToLoad != null) { if (urlToLoad != null) {
loadUrl(urlToLoad, anchorToLoad); loadUrl(urlToLoad);
} else { } else {
loadChapterUrl(chapterToLoad, anchorToLoad); loadChapterUrl(chapterToLoad);
} }
} }
})(jQuery); })(jQuery);
...@@ -92,9 +82,6 @@ ...@@ -92,9 +82,6 @@
%if chapter is not None: %if chapter is not None:
options.chapterNum = ${chapter}; options.chapterNum = ${chapter};
%endif %endif
%if anchor_id is not None:
options.anchor_id = ${anchor_id};
%endif
$('#outerContainer').myHTMLViewer(options); $('#outerContainer').myHTMLViewer(options);
}); });
......
...@@ -264,10 +264,6 @@ if settings.COURSEWARE_ENABLED: ...@@ -264,10 +264,6 @@ if settings.COURSEWARE_ENABLED:
'staticbook.views.html_index', name="html_book"), 'staticbook.views.html_index', name="html_book"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/$',
'staticbook.views.html_index'), 'staticbook.views.html_index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/(?P<anchor_id>[^/]*)/$',
'staticbook.views.html_index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/(?P<anchor_id>[^/]*)/$',
'staticbook.views.html_index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$',
'courseware.views.index', name="courseware"), 'courseware.views.index', name="courseware"),
......
...@@ -48,10 +48,11 @@ def django_for_jasmine(system, django_reload) ...@@ -48,10 +48,11 @@ def django_for_jasmine(system, django_reload)
reload_arg = '--noreload' reload_arg = '--noreload'
end end
port = 10000 + rand(40000)
django_pid = fork do django_pid = fork do
exec(*django_admin(system, 'jasmine', 'runserver', '-v', '0', "12345", reload_arg).split(' ')) exec(*django_admin(system, 'jasmine', 'runserver', '-v', '0', port.to_s, reload_arg).split(' '))
end end
jasmine_url = 'http://localhost:12345/_jasmine/' jasmine_url = "http://localhost:#{port}/_jasmine/"
up = false up = false
start_time = Time.now start_time = Time.now
until up do until up do
...@@ -269,7 +270,7 @@ Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib| ...@@ -269,7 +270,7 @@ Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib|
desc "Run tests for common lib #{lib}" desc "Run tests for common lib #{lib}"
task task_name => report_dir do task task_name => report_dir do
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
cmd = "nosetests #{lib} --logging-clear-handlers --with-xunit" cmd = "nosetests #{lib}"
sh(run_under_coverage(cmd, lib)) do |ok, res| sh(run_under_coverage(cmd, lib)) do |ok, res|
$failed_tests += 1 unless ok $failed_tests += 1 unless ok
end end
......
...@@ -4,9 +4,7 @@ beautifulsoup==3.2.1 ...@@ -4,9 +4,7 @@ beautifulsoup==3.2.1
boto==2.6.0 boto==2.6.0
django-celery==3.0.11 django-celery==3.0.11
django-countries==1.5 django-countries==1.5
django-debug-toolbar-mongo
django-followit==0.0.3 django-followit==0.0.3
django-jasmine==0.3.2
django-keyedcache==1.4-6 django-keyedcache==1.4-6
django-kombu==0.9.4 django-kombu==0.9.4
django-mako==0.1.5pre django-mako==0.1.5pre
...@@ -19,29 +17,20 @@ django-ses==0.4.1 ...@@ -19,29 +17,20 @@ django-ses==0.4.1
django-storages==1.1.5 django-storages==1.1.5
django-threaded-multihost==1.4-1 django-threaded-multihost==1.4-1
django==1.4.3 django==1.4.3
django_debug_toolbar
django_nose==1.1
dogapi==1.2.1
dogstatsd-python==0.2.1
factory_boy
feedparser==5.1.3 feedparser==5.1.3
fs==0.4.0 fs==0.4.0
GitPython==0.3.2.RC1 GitPython==0.3.2.RC1
glob2==0.3 glob2==0.3
http://sympy.googlecode.com/files/sympy-0.7.1.tar.gz http://sympy.googlecode.com/files/sympy-0.7.1.tar.gz
ipython==0.13.1
lxml==3.0.1 lxml==3.0.1
mako==0.7.3 mako==0.7.3
Markdown==2.2.1 Markdown==2.2.1
mock==0.8.0
MySQL-python==1.2.4c1 MySQL-python==1.2.4c1
networkx==1.7 networkx==1.7
newrelic==1.8.0.13
nltk==2.0.4 nltk==2.0.4
nosexcover==1.0.7
numpy==1.6.2 numpy==1.6.2
paramiko==1.9.0 paramiko==1.9.0
path.py path.py==3.0.1
Pillow==1.7.8 Pillow==1.7.8
pip pip
pygments==1.5 pygments==1.5
...@@ -51,11 +40,37 @@ python-memcached==1.48 ...@@ -51,11 +40,37 @@ python-memcached==1.48
python-openid==2.2.5 python-openid==2.2.5
pytz==2012h pytz==2012h
PyYAML==3.10 PyYAML==3.10
rednose==0.3
requests==0.14.2 requests==0.14.2
scipy==0.11.0 scipy==0.11.0
Shapely==1.2.16 Shapely==1.2.16
sorl-thumbnail==11.12 sorl-thumbnail==11.12
South==0.7.6 South==0.7.6
sphinx==1.1.3
xmltodict==0.4.1 xmltodict==0.4.1
# Used for debugging
ipython==0.13.1
# Metrics gathering and monitoring
dogapi==1.2.1
dogstatsd-python==0.2.1
newrelic==1.8.0.13
# Used for documentation gathering
sphinx==1.1.3
# Used for testing
coverage==3.6
factory_boy==2.0.2
lettuce==0.2.16
mock==0.8.0
nosexcover==1.0.7
pep8==1.4.5
pylint==0.27.0
rednose==0.3
selenium==2.31.0
splinter==0.5.0
django_nose==1.1
django-jasmine==0.3.2
django_debug_toolbar
django-debug-toolbar-mongo
[nosetests]
logging-clear-handlers=1
with-xunit=1
rednose=1
# Uncomment the following line to open pdb when a test fails
#pdb=1
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment