Commit 9a7b6c7e by Brian Talbot

Merge branch 'master' into feature/btalbot/studio-alerts

parents 156a702e 7af36eb6
......@@ -11,6 +11,14 @@ Feature: Create Section
And I see a release date for my section
And I see a link to create a new subsection
Scenario: Add a new section (with a quote in the name) to a course (bug #216)
Given I have opened a new course in Studio
When I click the New Section link
And I enter a section name with a quote and click save
Then I see my section name with a quote on the Courseware page
And I click to edit the section name
Then I see the complete section name with a quote in the editor
Scenario: Edit section release date
Given I have opened a new course in Studio
And I have added a new section
......
from lettuce import world, step
from common import *
from nose.tools import assert_equal
############### ACTIONS ####################
......@@ -12,10 +13,12 @@ def i_click_new_section_link(step):
@step('I enter the section name and click save$')
def i_save_section_name(step):
name_css = '.new-section-name'
save_css = '.new-section-name-save'
css_fill(name_css, 'My Section')
css_click(save_css)
save_section_name('My Section')
@step('I enter a section name with a quote and click save$')
def i_save_section_name_with_quote(step):
save_section_name('Section with "Quote"')
@step('I have added a new section$')
......@@ -45,8 +48,24 @@ def i_save_a_new_section_release_date(step):
@step('I see my section on the Courseware page$')
def i_see_my_section_on_the_courseware_page(step):
section_css = 'span.section-name-span'
assert_css_with_text(section_css, 'My Section')
see_my_section_on_the_courseware_page('My Section')
@step('I see my section name with a quote on the Courseware page$')
def i_see_my_section_name_with_quote_on_the_courseware_page(step):
see_my_section_on_the_courseware_page('Section with "Quote"')
@step('I click to edit the section name$')
def i_click_to_edit_section_name(step):
css_click('span.section-name-span')
@step('I see the complete section name with a quote in the editor$')
def i_see_complete_section_name_with_quote_in_editor(step):
css = '.edit-section-name'
assert world.browser.is_element_present_by_css(css, 5)
assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"')
@step('the section does not exist$')
......@@ -88,3 +107,17 @@ def the_section_release_date_is_updated(step):
css = 'span.published-status'
status_text = world.browser.find_by_css(css).text
assert status_text == 'Will Release: 12/25/2013 at 12:00am'
############ HELPER METHODS ###################
def save_section_name(name):
name_css = '.new-section-name'
save_css = '.new-section-name-save'
css_fill(name_css, name)
css_click(save_css)
def see_my_section_on_the_courseware_page(name):
section_css = 'span.section-name-span'
assert_css_with_text(section_css, name)
\ No newline at end of file
......@@ -9,6 +9,14 @@ Feature: Create Subsection
And I enter the subsection name and click save
Then I see my subsection on the Courseware page
Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216)
Given I have opened a new course section in Studio
When I click the New Subsection link
And I enter a subsection name with a quote and click save
Then I see my subsection name with a quote on the Courseware page
And I click to edit the subsection name
Then I see the complete subsection name with a quote in the editor
Scenario: Delete a subsection
Given I have opened a new course section in Studio
And I have added a new subsection
......
from lettuce import world, step
from common import *
from nose.tools import assert_equal
############### ACTIONS ####################
......@@ -20,28 +21,60 @@ def i_click_the_new_subsection_link(step):
@step('I enter the subsection name and click save$')
def i_save_subsection_name(step):
name_css = 'input.new-subsection-name-input'
save_css = 'input.new-subsection-name-save'
css_fill(name_css, 'Subsection One')
css_click(save_css)
save_subsection_name('Subsection One')
@step('I enter a subsection name with a quote and click save$')
def i_save_subsection_name_with_quote(step):
save_subsection_name('Subsection With "Quote"')
@step('I click to edit the subsection name$')
def i_click_to_edit_subsection_name(step):
css_click('span.subsection-name-value')
@step('I see the complete subsection name with a quote in the editor$')
def i_see_complete_subsection_name_with_quote_in_editor(step):
css = '.subsection-display-name-input'
assert world.browser.is_element_present_by_css(css, 5)
assert_equal(world.browser.find_by_css(css).value, 'Subsection With "Quote"')
@step('I have added a new subsection$')
def i_have_added_a_new_subsection(step):
add_subsection()
############ ASSERTIONS ###################
@step('I see my subsection on the Courseware page$')
def i_see_my_subsection_on_the_courseware_page(step):
css = 'span.subsection-name'
assert world.browser.is_element_present_by_css(css)
css = 'span.subsection-name-value'
assert_css_with_text(css, 'Subsection One')
see_subsection_name('Subsection One')
@step('I see my subsection name with a quote on the Courseware page$')
def i_see_my_subsection_name_with_quote_on_the_courseware_page(step):
see_subsection_name('Subsection With "Quote"')
@step('the subsection does not exist$')
def the_subsection_does_not_exist(step):
css = 'span.subsection-name'
assert world.browser.is_element_not_present_by_css(css)
############ HELPER METHODS ###################
def save_subsection_name(name):
name_css = 'input.new-subsection-name-input'
save_css = 'input.new-subsection-name-save'
css_fill(name_css, name)
css_click(save_css)
def see_subsection_name(name):
css = 'span.subsection-name'
assert world.browser.is_element_present_by_css(css)
css = 'span.subsection-name-value'
assert_css_with_text(css, name)
from factory import Factory
from datetime import datetime
from uuid import uuid4
from student.models import (User, UserProfile, Registration,
CourseEnrollmentAllowed)
from django.contrib.auth.models import Group
class UserProfileFactory(Factory):
FACTORY_FOR = UserProfile
user = None
name = 'Robot Studio'
courseware = 'course.xml'
class RegistrationFactory(Factory):
FACTORY_FOR = Registration
user = None
activation_key = uuid4().hex
class UserFactory(Factory):
FACTORY_FOR = User
username = 'robot'
email = 'robot@edx.org'
password = 'test'
first_name = 'Robot'
last_name = 'Tester'
is_staff = False
is_active = True
is_superuser = False
last_login = datetime.now()
date_joined = datetime.now()
class GroupFactory(Factory):
FACTORY_FOR = Group
name = 'test_group'
class CourseEnrollmentAllowedFactory(Factory):
FACTORY_FOR = CourseEnrollmentAllowed
email = 'test@edx.org'
course_id = 'edX/test/2012_Fall'
......@@ -68,6 +68,10 @@ log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
ADVANCED_COMPONENT_TYPES = ['annotatable','combinedopenended', 'peergrading']
ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
......@@ -284,10 +288,31 @@ def edit_unit(request, location):
component_templates = defaultdict(list)
# Check if there are any advanced modules specified in the course policy. These modules
# should be specified as a list of strings, where the strings are the names of the modules
# in ADVANCED_COMPONENT_TYPES that should be enabled for the course.
course_metadata = CourseMetadata.fetch(course.location)
course_advanced_keys = course_metadata.get(ADVANCED_COMPONENT_POLICY_KEY, [])
# Set component types according to course policy file
component_types = list(COMPONENT_TYPES)
if isinstance(course_advanced_keys, list):
course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES]
if len(course_advanced_keys) > 0:
component_types.append(ADVANCED_COMPONENT_CATEGORY)
else:
log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys))
templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
for template in templates:
if template.location.category in COMPONENT_TYPES:
component_templates[template.location.category].append((
category = template.location.category
if category in course_advanced_keys:
category = ADVANCED_COMPONENT_CATEGORY
if category in component_types:
#This is a hack to create categories for different xmodules
component_templates[category].append((
template.display_name,
template.location.url(),
'markdown' in template.metadata,
......
......@@ -27,6 +27,9 @@ STATIC_ROOT = TEST_ROOT / "staticfiles"
GITHUB_REPO_ROOT = TEST_ROOT / "data"
COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data"
# Makes the tests run much faster...
SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
# 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 = [
COMMON_ROOT / "static",
......
......@@ -254,6 +254,30 @@
background: url(../img/html-icon.png) center no-repeat;
}
.large-openended-icon {
display: inline-block;
width: 100px;
height: 60px;
margin-right: 5px;
background: url(../img/large-openended-icon.png) center no-repeat;
}
.large-annotations-icon {
display: inline-block;
width: 100px;
height: 60px;
margin-right: 5px;
background: url(../img/large-annotations-icon.png) center no-repeat;
}
.large-advanced-icon {
display: inline-block;
width: 100px;
height: 60px;
margin-right: 5px;
background: url(../img/large-advanced-icon.png) center no-repeat;
}
.large-textbook-icon {
display: inline-block;
width: 100px;
......
......@@ -28,7 +28,7 @@
{{uploadDate}}
</td>
<td class="embed-col">
<input type="text" class="embeddable-xml-input" value='{{url}}' disabled>
<input type="text" class="embeddable-xml-input" value='{{url}}' readonly>
</td>
</tr>
</script>
......@@ -84,7 +84,7 @@
${asset['uploadDate']}
</td>
<td class="embed-col">
<input type="text" class="embeddable-xml-input" value="${asset['url']}" disabled>
<input type="text" class="embeddable-xml-input" value="${asset['url']}" readonly>
</td>
</tr>
% endfor
......@@ -115,7 +115,7 @@
</div>
<div class="embeddable">
<label>URL:</label>
<input type="text" class="embeddable-xml-input" value='' disabled>
<input type="text" class="embeddable-xml-input" value='' readonly>
</div>
<form class="file-chooser" action="${upload_asset_callback_url}"
method="post" enctype="multipart/form-data">
......
......@@ -146,6 +146,13 @@ class LoncapaProblem(object):
if not self.student_answers: # True when student_answers is an empty dict
self.set_initial_display()
# dictionary of InputType objects associated with this problem
# input_id string -> InputType object
self.inputs = {}
self.extracted_tree = self._extract_html(self.tree)
def do_reset(self):
'''
Reset internal state to unfinished, with no answers
......@@ -324,7 +331,27 @@ class LoncapaProblem(object):
'''
Main method called externally to get the HTML to be rendered for this capa Problem.
'''
return contextualize_text(etree.tostring(self._extract_html(self.tree)), self.context)
html = contextualize_text(etree.tostring(self._extract_html(self.tree)), self.context)
return html
def handle_input_ajax(self, get):
'''
InputTypes can support specialized AJAX calls. Find the correct input and pass along the correct data
Also, parse out the dispatch from the get so that it can be passed onto the input type nicely
'''
# pull out the id
input_id = get['input_id']
if self.inputs[input_id]:
dispatch = get['dispatch']
return self.inputs[input_id].handle_ajax(dispatch, get)
else:
log.warning("Could not find matching input for id: %s" % problem_id)
return {}
# ======= Private Methods Below ========
......@@ -458,6 +485,8 @@ class LoncapaProblem(object):
finally:
sys.path = original_path
def _extract_html(self, problemtree): # private
'''
Main (private) function which converts Problem XML tree to HTML.
......@@ -468,6 +497,7 @@ class LoncapaProblem(object):
Used by get_html.
'''
if (problemtree.tag == 'script' and problemtree.get('type')
and 'javascript' in problemtree.get('type')):
# leave javascript intact.
......@@ -484,8 +514,9 @@ class LoncapaProblem(object):
msg = ''
hint = ''
hintmode = None
input_id = problemtree.get('id')
if problemid in self.correct_map:
pid = problemtree.get('id')
pid = input_id
status = self.correct_map.get_correctness(pid)
msg = self.correct_map.get_msg(pid)
hint = self.correct_map.get_hint(pid)
......@@ -496,17 +527,17 @@ class LoncapaProblem(object):
value = self.student_answers[problemid]
# do the rendering
state = {'value': value,
'status': status,
'id': problemtree.get('id'),
'id': input_id,
'feedback': {'message': msg,
'hint': hint,
'hintmode': hintmode, }}
input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag)
the_input = input_type_cls(self.system, problemtree, state)
return the_input.get_html()
# save the input type so that we can make ajax calls on it if we need to
self.inputs[input_id] = input_type_cls(self.system, problemtree, state)
return self.inputs[input_id].get_html()
# let each Response render itself
if problemtree in self.responders:
......
......@@ -215,6 +215,18 @@ class InputTypeBase(object):
"""
pass
def handle_ajax(self, dispatch, get):
"""
InputTypes that need to handle specialized AJAX should override this.
Input:
dispatch: a string that can be used to determine how to handle the data passed in
get: a dictionary containing the data that was sent with the ajax call
Output:
a dictionary object that can be serialized into JSON. This will be sent back to the Javascript.
"""
pass
def _get_render_context(self):
"""
......
......@@ -125,6 +125,8 @@ class CapaHtmlRenderTest(unittest.TestCase):
expected_solution_context = {'id': '1_solution_1'}
expected_calls = [mock.call('textline.html', expected_textline_context),
mock.call('solutionspan.html', expected_solution_context),
mock.call('textline.html', expected_textline_context),
mock.call('solutionspan.html', expected_solution_context)]
self.assertEqual(test_system.render_template.call_args_list,
......
......@@ -35,7 +35,8 @@ class StaticContent(object):
@staticmethod
def compute_location(org, course, name, revision=None, is_thumbnail=False):
name = name.replace('/', '_')
return Location([XASSET_LOCATION_TAG, org, course, 'asset' if not is_thumbnail else 'thumbnail', Location.clean(name), revision])
return Location([XASSET_LOCATION_TAG, org, course, 'asset' if not is_thumbnail else 'thumbnail',
Location.clean_keeping_underscores(name), revision])
def get_id(self):
return StaticContent.get_id_from_location(self.location)
......
......@@ -76,6 +76,24 @@ class @Problem
# TODO: Some logic to dynamically adjust polling rate based on queuelen
window.queuePollerID = window.setTimeout(@poll, 1000)
# Use this if you want to make an ajax call on the input type object
# static method so you don't have to instantiate a Problem in order to use it
# Input:
# url: the AJAX url of the problem
# input_id: the input_id of the input you would like to make the call on
# NOTE: the id is the ${id} part of "input_${id}" during rendering
# If this function is passed the entire prefixed id, the backend may have trouble
# finding the correct input
# dispatch: string that indicates how this data should be handled by the inputtype
# callback: the function that will be called once the AJAX call has been completed.
# It will be passed a response object
@inputAjax: (url, input_id, dispatch, data, callback) ->
data['dispatch'] = dispatch
data['input_id'] = input_id
$.postWithPrefix "#{url}/input_ajax", data, callback
render: (content) ->
if content
@el.html(content)
......
......@@ -71,6 +71,17 @@ class Location(_LocationBase):
"""
return Location._clean(value, INVALID_CHARS)
@staticmethod
def clean_keeping_underscores(value):
"""
Return value, replacing INVALID_CHARS, but not collapsing multiple '_' chars.
This for cleaning asset names, as the YouTube ID's may have underscores in them, and we need the
transcript asset name to match. In the future we may want to change the behavior of _clean.
"""
return INVALID_CHARS.sub('_', value)
@staticmethod
def clean_for_url_name(value):
"""
......
......@@ -79,6 +79,9 @@ class CombinedOpenEndedV1Module():
INTERMEDIATE_DONE = 'intermediate_done'
DONE = 'done'
#Where the templates live for this problem
TEMPLATE_DIR = "combinedopenended"
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, metadata = None, static_data = None, **kwargs):
......@@ -343,7 +346,7 @@ class CombinedOpenEndedV1Module():
Output: rendered html
"""
context = self.get_context()
html = self.system.render_template('combined_open_ended.html', context)
html = self.system.render_template('{0}/combined_open_ended.html'.format(self.TEMPLATE_DIR), context)
return html
def get_html_nonsystem(self):
......@@ -354,7 +357,7 @@ class CombinedOpenEndedV1Module():
Output: HTML rendered directly via Mako
"""
context = self.get_context()
html = self.system.render_template('combined_open_ended.html', context)
html = self.system.render_template('{0}/combined_open_ended.html'.format(self.TEMPLATE_DIR), context)
return html
def get_html_base(self):
......@@ -531,7 +534,7 @@ class CombinedOpenEndedV1Module():
'task_name' : 'Scored Rubric',
'class_name' : 'combined-rubric-container'
}
html = self.system.render_template('combined_open_ended_results.html', context)
html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context)
return {'html': html, 'success': True}
def get_legend(self, get):
......@@ -543,7 +546,7 @@ class CombinedOpenEndedV1Module():
context = {
'legend_list' : LEGEND_LIST,
}
html = self.system.render_template('combined_open_ended_legend.html', context)
html = self.system.render_template('{0}/combined_open_ended_legend.html'.format(self.TEMPLATE_DIR), context)
return {'html': html, 'success': True}
def get_results(self, get):
......@@ -574,7 +577,7 @@ class CombinedOpenEndedV1Module():
'submission_id' : ri['submission_ids'][i],
}
context_list.append(context)
feedback_table = self.system.render_template('open_ended_result_table.html', {
feedback_table = self.system.render_template('{0}/open_ended_result_table.html'.format(self.TEMPLATE_DIR), {
'context_list' : context_list,
'grader_type_image_dict' : GRADER_TYPE_IMAGE_DICT,
'human_grader_types' : HUMAN_GRADER_TYPE,
......@@ -586,7 +589,7 @@ class CombinedOpenEndedV1Module():
'task_name' : "Feedback",
'class_name' : "result-container",
}
html = self.system.render_template('combined_open_ended_results.html', context)
html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context)
return {'html': html, 'success': True}
def get_status_ajax(self, get):
......@@ -700,7 +703,7 @@ class CombinedOpenEndedV1Module():
'legend_list' : LEGEND_LIST,
'render_via_ajax' : render_via_ajax,
}
status_html = self.system.render_template("combined_open_ended_status.html", context)
status_html = self.system.render_template("{0}/combined_open_ended_status.html".format(self.TEMPLATE_DIR), context)
return status_html
......
......@@ -30,6 +30,8 @@ class RubricParsingError(Exception):
class CombinedOpenEndedRubric(object):
TEMPLATE_DIR = "combinedopenended/openended"
def __init__ (self, system, view_only = False):
self.has_score = False
self.view_only = view_only
......@@ -57,9 +59,9 @@ class CombinedOpenEndedRubric(object):
rubric_scores = [cat['score'] for cat in rubric_categories]
max_scores = map((lambda cat: cat['options'][-1]['points']), rubric_categories)
max_score = max(max_scores)
rubric_template = 'open_ended_rubric.html'
rubric_template = '{0}/open_ended_rubric.html'.format(self.TEMPLATE_DIR)
if self.view_only:
rubric_template = 'open_ended_view_only_rubric.html'
rubric_template = '{0}/open_ended_view_only_rubric.html'.format(self.TEMPLATE_DIR)
html = self.system.render_template(rubric_template,
{'categories': rubric_categories,
'has_score': self.has_score,
......@@ -207,7 +209,7 @@ class CombinedOpenEndedRubric(object):
for grader_type in tuple[3]:
rubric_categories[i]['options'][j]['grader_types'].append(grader_type)
html = self.system.render_template('open_ended_combined_rubric.html',
html = self.system.render_template('{0}/open_ended_combined_rubric.html'.format(self.TEMPLATE_DIR),
{'categories': rubric_categories,
'has_score': True,
'view_only': True,
......
......@@ -40,6 +40,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
</openended>
"""
TEMPLATE_DIR = "combinedopenended/openended"
def setup_response(self, system, location, definition, descriptor):
"""
Sets up the response type.
......@@ -397,10 +399,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
rubric_scores = rubric_dict['rubric_scores']
if not response_items['success']:
return system.render_template("open_ended_error.html",
return system.render_template("{0}/open_ended_error.html".format(self.TEMPLATE_DIR),
{'errors': feedback})
feedback_template = system.render_template("open_ended_feedback.html", {
feedback_template = system.render_template("{0}/open_ended_feedback.html".format(self.TEMPLATE_DIR), {
'grader_type': response_items['grader_type'],
'score': "{0} / {1}".format(response_items['score'], self.max_score()),
'feedback': feedback,
......@@ -558,7 +560,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
@return: Rendered html
"""
context = {'msg': feedback, 'id': "1", 'rows': 50, 'cols': 50}
html = system.render_template('open_ended_evaluation.html', context)
html = system.render_template('{0}/open_ended_evaluation.html'.format(self.TEMPLATE_DIR), context)
return html
def handle_ajax(self, dispatch, get, system):
......@@ -692,7 +694,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'accept_file_upload': self.accept_file_upload,
'eta_message' : eta_string,
}
html = system.render_template('open_ended.html', context)
html = system.render_template('{0}/open_ended.html'.format(self.TEMPLATE_DIR), context)
return html
......
......@@ -32,6 +32,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
</selfassessment>
"""
TEMPLATE_DIR = "combinedopenended/selfassessment"
def setup_response(self, system, location, definition, descriptor):
"""
Sets up the module
......@@ -68,7 +70,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
'accept_file_upload': self.accept_file_upload,
}
html = system.render_template('self_assessment_prompt.html', context)
html = system.render_template('{0}/self_assessment_prompt.html'.format(self.TEMPLATE_DIR), context)
return html
......@@ -129,7 +131,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
#This is a dev_facing_error
raise ValueError("Self assessment module is in an illegal state '{0}'".format(self.state))
return system.render_template('self_assessment_rubric.html', context)
return system.render_template('{0}/self_assessment_rubric.html'.format(self.TEMPLATE_DIR), context)
def get_hint_html(self, system):
"""
......@@ -155,7 +157,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
#This is a dev_facing_error
raise ValueError("Self Assessment module is in an illegal state '{0}'".format(self.state))
return system.render_template('self_assessment_hint.html', context)
return system.render_template('{0}/self_assessment_hint.html'.format(self.TEMPLATE_DIR), context)
def save_answer(self, get, system):
......
......@@ -471,6 +471,9 @@ class PeerGradingModule(XModule):
#This is a student_facing_error
error_text = "Could not get list of problems to peer grade. Please notify course staff."
success = False
except:
log.exception("Could not contact peer grading service.")
success = False
def _find_corresponding_module_for_location(location):
......@@ -589,7 +592,6 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
'task_xml': dictionary of xml strings,
}
"""
log.debug("In definition")
expected_children = []
for child in expected_children:
if len(xml_object.xpath(child)) == 0:
......
---
metadata:
display_name: Open Ended Response
max_attempts: 1
max_score: 1
is_graded: False
version: 1
display_name: Open Ended Response
skip_spelling_checks: False
accept_file_upload: False
data: |
<combinedopenended>
<rubric>
<rubric>
<category>
<description>Category 1</description>
<option>
The response does not incorporate what is needed for a one response.
</option>
<option>
The response is correct for category 1.
</option>
</category>
</rubric>
</rubric>
<prompt>
<p>Why is the sky blue?</p>
</prompt>
<task>
<selfassessment/>
</task>
<task>
<openended min_score_to_attempt="1" max_score_to_attempt="2">
<openendedparam>
<initial_display>Enter essay here.</initial_display>
<answer_display>This is the answer.</answer_display>
<grader_payload>{"grader_settings" : "peer_grading.conf", "problem_id" : "700x/Demo"}</grader_payload>
</openendedparam>
</openended>
</task>
</combinedopenended>
children: []
---
metadata:
display_name: Peer Grading Interface
attempts: 1
use_for_single_location: False
link_to_location: None
is_graded: False
max_grade: 1
data: |
<peergrading>
</peergrading>
children: []
......@@ -28,13 +28,27 @@ open_ended_grading_interface = {
'grading_controller' : 'grading_controller'
}
test_system = ModuleSystem(
def test_system():
"""
Construct a test ModuleSystem instance.
By default, the render_template() method simply returns
the context it is passed as a string.
You can override this behavior by monkey patching:
system = test_system()
system.render_template = my_render_func
where my_render_func is a function of the form
my_render_func(template, context)
"""
return ModuleSystem(
ajax_url='courses/course_id/modx/a_location',
track_function=Mock(),
get_module=Mock(),
# "render" to just the context...
render_template=lambda template, context: str(context),
replace_urls=Mock(),
replace_urls=lambda html: str(html),
user=Mock(is_staff=False),
filestore=Mock(),
debug=True,
......@@ -42,7 +56,7 @@ test_system = ModuleSystem(
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
anonymous_student_id='student',
open_ended_grading_interface= open_ended_grading_interface
)
)
class ModelsTest(unittest.TestCase):
......
......@@ -54,7 +54,8 @@ class OpenEndedChildTest(unittest.TestCase):
descriptor = Mock()
def setUp(self):
self.openendedchild = OpenEndedChild(test_system, self.location,
self.test_system = test_system()
self.openendedchild = OpenEndedChild(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata)
......@@ -69,7 +70,7 @@ class OpenEndedChildTest(unittest.TestCase):
def test_latest_post_assessment_empty(self):
answer = self.openendedchild.latest_post_assessment(test_system)
answer = self.openendedchild.latest_post_assessment(self.test_system)
self.assertEqual(answer, "")
......@@ -106,7 +107,7 @@ class OpenEndedChildTest(unittest.TestCase):
post_assessment = "Post assessment"
self.openendedchild.record_latest_post_assessment(post_assessment)
self.assertEqual(post_assessment,
self.openendedchild.latest_post_assessment(test_system))
self.openendedchild.latest_post_assessment(self.test_system))
def test_get_score(self):
new_answer = "New Answer"
......@@ -125,7 +126,7 @@ class OpenEndedChildTest(unittest.TestCase):
def test_reset(self):
self.openendedchild.reset(test_system)
self.openendedchild.reset(self.test_system)
state = json.loads(self.openendedchild.get_instance_state())
self.assertEqual(state['state'], OpenEndedChild.INITIAL)
......@@ -182,11 +183,13 @@ class OpenEndedModuleTest(unittest.TestCase):
descriptor = Mock()
def setUp(self):
test_system.location = self.location
self.test_system = test_system()
self.test_system.location = self.location
self.mock_xqueue = MagicMock()
self.mock_xqueue.send_to_queue.return_value = (None, "Message")
test_system.xqueue = {'interface': self.mock_xqueue, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 1}
self.openendedmodule = OpenEndedModule(test_system, self.location,
self.test_system.xqueue = {'interface': self.mock_xqueue, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 1}
self.openendedmodule = OpenEndedModule(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata)
def test_message_post(self):
......@@ -195,7 +198,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'grader_id': '1',
'score': 3}
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
student_info = {'anonymous_student_id': test_system.anonymous_student_id,
student_info = {'anonymous_student_id': self.test_system.anonymous_student_id,
'submission_time': qtime}
contents = {
'feedback': get['feedback'],
......@@ -205,7 +208,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'student_info': json.dumps(student_info)
}
result = self.openendedmodule.message_post(get, test_system)
result = self.openendedmodule.message_post(get, self.test_system)
self.assertTrue(result['success'])
# make sure it's actually sending something we want to the queue
self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY)
......@@ -216,7 +219,7 @@ class OpenEndedModuleTest(unittest.TestCase):
def test_send_to_grader(self):
submission = "This is a student submission"
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
student_info = {'anonymous_student_id': test_system.anonymous_student_id,
student_info = {'anonymous_student_id': self.test_system.anonymous_student_id,
'submission_time': qtime}
contents = self.openendedmodule.payload.copy()
contents.update({
......@@ -224,7 +227,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'student_response': submission,
'max_score': self.max_score
})
result = self.openendedmodule.send_to_grader(submission, test_system)
result = self.openendedmodule.send_to_grader(submission, self.test_system)
self.assertTrue(result)
self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY)
......@@ -238,7 +241,7 @@ class OpenEndedModuleTest(unittest.TestCase):
}
get = {'queuekey': "abcd",
'xqueue_body': score_msg}
self.openendedmodule.update_score(get, test_system)
self.openendedmodule.update_score(get, self.test_system)
def update_score_single(self):
self.openendedmodule.new_history_entry("New Entry")
......@@ -261,11 +264,11 @@ class OpenEndedModuleTest(unittest.TestCase):
}
get = {'queuekey': "abcd",
'xqueue_body': json.dumps(score_msg)}
self.openendedmodule.update_score(get, test_system)
self.openendedmodule.update_score(get, self.test_system)
def test_latest_post_assessment(self):
self.update_score_single()
assessment = self.openendedmodule.latest_post_assessment(test_system)
assessment = self.openendedmodule.latest_post_assessment(self.test_system)
self.assertFalse(assessment == '')
# check for errors
self.assertFalse('errors' in assessment)
......@@ -336,7 +339,13 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
descriptor = Mock()
def setUp(self):
self.combinedoe = CombinedOpenEndedV1Module(test_system, self.location, self.definition, self.descriptor, static_data = self.static_data, metadata=self.metadata)
self.test_system = test_system()
self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
self.location,
self.definition,
self.descriptor,
static_data = self.static_data,
metadata=self.metadata)
def test_get_tag_name(self):
name = self.combinedoe.get_tag_name("<t>Tag</t>")
......
......@@ -56,6 +56,9 @@ class ConditionalModuleTest(unittest.TestCase):
'''Get a dummy system'''
return DummySystem(load_error_modules)
def setUp(self):
self.test_system = test_system()
def get_course(self, name):
"""Get a test course by directory name. If there's more than one, error."""
print "Importing {0}".format(name)
......@@ -85,14 +88,14 @@ class ConditionalModuleTest(unittest.TestCase):
location = descriptor.location
instance_state = instance_states.get(location.category, None)
print "inner_get_module, location=%s, inst_state=%s" % (location, instance_state)
return descriptor.xmodule_constructor(test_system)(instance_state, shared_state)
return descriptor.xmodule_constructor(self.test_system)(instance_state, shared_state)
location = Location(["i4x", "edX", "cond_test", "conditional", "condone"])
def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None):
return text
test_system.replace_urls = replace_urls
test_system.get_module = inner_get_module
self.test_system.replace_urls = replace_urls
self.test_system.get_module = inner_get_module
module = inner_get_module(location)
print "module: ", module
......
......@@ -19,9 +19,14 @@ class ContentTest(unittest.TestCase):
content = StaticContent('loc', 'name', 'content_type', 'data')
self.assertIsNone(content.thumbnail_location)
def test_generate_thumbnail_nonimage(self):
def test_generate_thumbnail_image(self):
contentStore = ContentStore()
content = Content(Location(u'c4x', u'mitX', u'800', u'asset', u'monsters.jpg'), None)
content = Content(Location(u'c4x', u'mitX', u'800', u'asset', u'monsters__.jpg'), None)
(thumbnail_content, thumbnail_file_location) = contentStore.generate_thumbnail(content)
self.assertIsNone(thumbnail_content)
self.assertEqual(Location(u'c4x', u'mitX', u'800', u'thumbnail', u'monsters.jpg'), thumbnail_file_location)
self.assertEqual(Location(u'c4x', u'mitX', u'800', u'thumbnail', u'monsters__.jpg'), thumbnail_file_location)
def test_compute_location(self):
# We had a bug that __ got converted into a single _. Make sure that substitution of INVALID_CHARS (like space)
# still happen.
asset_location = StaticContent.compute_location('mitX', '400', 'subs__1eo_jXvZnE .srt.sjson')
self.assertEqual(Location(u'c4x', u'mitX', u'400', u'asset', u'subs__1eo_jXvZnE_.srt.sjson', None), asset_location)
......@@ -53,13 +53,13 @@ class SelfAssessmentTest(unittest.TestCase):
'skip_basic_checks' : False,
}
self.module = SelfAssessmentModule(test_system, self.location,
self.module = SelfAssessmentModule(test_system(), self.location,
self.definition, self.descriptor,
static_data,
state, metadata=self.metadata)
def test_get_html(self):
html = self.module.get_html(test_system)
html = self.module.get_html(self.module.system)
self.assertTrue("This is sample prompt text" in html)
def test_self_assessment_flow(self):
......@@ -82,10 +82,11 @@ class SelfAssessmentTest(unittest.TestCase):
self.assertEqual(self.module.get_score()['score'], 0)
self.module.save_answer({'student_answer': "I am an answer"}, test_system)
self.module.save_answer({'student_answer': "I am an answer"},
self.module.system)
self.assertEqual(self.module.state, self.module.ASSESSING)
self.module.save_assessment(mock_query_dict, test_system)
self.module.save_assessment(mock_query_dict, self.module.system)
self.assertEqual(self.module.state, self.module.DONE)
......@@ -94,7 +95,8 @@ class SelfAssessmentTest(unittest.TestCase):
self.assertEqual(self.module.state, self.module.INITIAL)
# if we now assess as right, skip the REQUEST_HINT state
self.module.save_answer({'student_answer': 'answer 4'}, test_system)
self.module.save_answer({'student_answer': 'answer 4'},
self.module.system)
responses['assessment'] = '1'
self.module.save_assessment(mock_query_dict, test_system)
self.module.save_assessment(mock_query_dict, self.module.system)
self.assertEqual(self.module.state, self.module.DONE)
......@@ -357,6 +357,8 @@ Supported fields at the course level
* `cohorted_discussions`: list of discussions that should be cohorted. Any not specified in this list are not cohorted.
* `auto_cohort`: Truthy.
* `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`
- have pdf-based textbooks on tabs in the courseware. See below for details on config.
Available metadata
......@@ -508,13 +510,15 @@ If you want to customize the courseware tabs displayed for your course, specify
"url_slug": "news",
"name": "Exciting news"
},
{"type": "textbooks"}
{"type": "textbooks"},
{"type": "pdf_textbooks"}
]
* If you specify any tabs, you must specify all tabs. They will appear in the order given.
* 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 `textbooks` tab will actually generate one tab per textbook, using the textbook titles as names.
* 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.
* An Instructor tab will be automatically added at the end for course staff users.
......@@ -527,13 +531,15 @@ If you want to customize the courseware tabs displayed for your course, specify
* - `course_info`
- Parameter `name`.
* - `wiki`
- arameter `name`.
- Parameter `name`.
* - `discussion`
- Parameter `name`.
* - `external_link`
- Parameters `name`, `link`.
* - `textbooks`
- No parameters--generates tab names from book titles.
* - `pdf_textbooks`
- No parameters--generates tab names from pdf book definition. (See discussion below for configuration.)
* - `progress`
- Parameter `name`.
* - `static_tab`
......@@ -541,6 +547,39 @@ If you want to customize the courseware tabs displayed for your course, specify
* - `staff_grading`
- No parameters. If specified, displays the staff grading tab for instructors.
*********
Textbooks
*********
Support is currently provided for image-based and PDF-based textbooks.
Image-based Textbooks
^^^^^^^^^^^^^^^^^^^^^
TBD.
PDF-based Textbooks
^^^^^^^^^^^^^^^^^^^
PDF-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 PDF-based material. The first way is as a single PDF on a tab, which requires only a tab title and a URL for configuration. A second way permits the display of multiple PDFs 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 PDF to view.
.. code-block:: json
"pdf_textbooks": [
{"tab_title": "Textbook 1",
"url": "https://www.example.com/book1.pdf" },
{"tab_title": "Textbook 2",
"chapters": [
{ "title": "Chapter 1", "url": "https://www.example.com/Chapter1.pdf" },
{ "title": "Chapter 2", "url": "https://www.example.com/Chapter2.pdf" },
{ "title": "Chapter 3", "url": "https://www.example.com/Chapter3.pdf" },
{ "title": "Chapter 4", "url": "https://www.example.com/Chapter4.pdf" },
{ "title": "Chapter 5", "url": "https://www.example.com/Chapter5.pdf" },
{ "title": "Chapter 6", "url": "https://www.example.com/Chapter6.pdf" },
{ "title": "Chapter 7", "url": "https://www.example.com/Chapter7.pdf" }
]
}
]
*************************************
Other file locations (info and about)
*************************************
......
import unittest
import logging
import time
from mock import Mock
from mock import Mock, MagicMock, patch
from django.conf import settings
from django.test import TestCase
from xmodule.course_module import CourseDescriptor
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore import Location
from factories import CourseEnrollmentAllowedFactory
from xmodule.timeparse import parse_time
from xmodule.x_module import XModule, XModuleDescriptor
import courseware.access as access
from factories import CourseEnrollmentAllowedFactory
class AccessTestCase(TestCase):
......
import logging
from mock import MagicMock, patch
import json
import factory
import unittest
from nose.tools import set_trace
from django.http import Http404, HttpResponse, HttpRequest
from django.conf import settings
from django.contrib.auth.models import User
from django.test.client import Client
from django.conf import settings
from django.test import TestCase
from django.test.client import RequestFactory
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from courseware.models import StudentModule, StudentModuleCache
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.exceptions import NotFoundError
from xmodule.modulestore import Location
import courseware.module_render as render
from xmodule.modulestore.django import modulestore, _MODULESTORES
from xmodule.seq_module import SequenceModule
from courseware.tests.tests import PageLoader
from student.models import Registration
from factories import UserFactory
class Stub:
def __init__(self):
pass
def xml_store_config(data_dir):
return {
'default': {
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
'OPTIONS': {
'data_dir': data_dir,
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
}
}
}
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class ModuleRenderTestCase(PageLoader):
def setUp(self):
self.location = ['i4x', 'edX', 'toy', 'chapter', 'Overview']
self._MODULESTORES = {}
self.course_id = 'edX/toy/2012_Fall'
self.toy_course = modulestore().get_course(self.course_id)
def test_get_module(self):
self.assertIsNone(render.get_module('dummyuser', None,
'invalid location', None, None))
def test_get_instance_module(self):
mock_user = MagicMock()
mock_user.is_authenticated.return_value = False
self.assertIsNone(render.get_instance_module('dummy', mock_user, 'dummy',
'dummy'))
mock_user_2 = MagicMock()
mock_user_2.is_authenticated.return_value = True
mock_module = MagicMock()
mock_module.descriptor.stores_state = False
self.assertIsNone(render.get_instance_module('dummy', mock_user_2,
mock_module, 'dummy'))
def test_modx_dispatch(self):
self.assertRaises(Http404, render.modx_dispatch, 'dummy', 'dummy',
'invalid Location', 'dummy')
mock_request = MagicMock()
mock_request.FILES.keys.return_value = ['file_id']
mock_request.FILES.getlist.return_value = ['file'] * (settings.MAX_FILEUPLOADS_PER_INPUT + 1)
self.assertEquals(render.modx_dispatch(mock_request, 'dummy', self.location,
'dummy').content,
json.dumps({'success': 'Submission aborted! Maximum %d files may be submitted at once' %
settings.MAX_FILEUPLOADS_PER_INPUT}))
mock_request_2 = MagicMock()
mock_request_2.FILES.keys.return_value = ['file_id']
inputfile = Stub()
inputfile.size = 1 + settings.STUDENT_FILEUPLOAD_MAX_SIZE
inputfile.name = 'name'
filelist = [inputfile]
mock_request_2.FILES.getlist.return_value = filelist
self.assertEquals(render.modx_dispatch(mock_request_2, 'dummy', self.location,
'dummy').content,
json.dumps({'success': 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))}))
mock_request_3 = MagicMock()
mock_request_3.POST.copy.return_value = {}
mock_request_3.FILES = False
mock_request_3.user = UserFactory()
inputfile_2 = Stub()
inputfile_2.size = 1
inputfile_2.name = 'name'
self.assertRaises(ItemNotFoundError, render.modx_dispatch,
mock_request_3, 'dummy', self.location, 'toy')
self.assertRaises(Http404, render.modx_dispatch, mock_request_3, 'dummy',
self.location, self.course_id)
mock_request_3.POST.copy.return_value = {'position': 1}
self.assertIsInstance(render.modx_dispatch(mock_request_3, 'goto_position',
self.location, self.course_id), HttpResponse)
def test_get_score_bucket(self):
self.assertEquals(render.get_score_bucket(0, 10), 'incorrect')
self.assertEquals(render.get_score_bucket(1, 10), 'partial')
self.assertEquals(render.get_score_bucket(10, 10), 'correct')
# get_score_bucket calls error cases 'incorrect'
self.assertEquals(render.get_score_bucket(11, 10), 'incorrect')
self.assertEquals(render.get_score_bucket(-1, 10), 'incorrect')
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestTOC(TestCase):
"""Check the Table of Contents for a course"""
def setUp(self):
self._MODULESTORES = {}
# Toy courses should be loaded
self.course_name = 'edX/toy/2012_Fall'
self.toy_course = modulestore().get_course(self.course_name)
self.portal_user = UserFactory()
def test_toc_toy_from_chapter(self):
chapter = 'Overview'
chapter_url = '%s/%s/%s' % ('/courses', self.course_name, chapter)
factory = RequestFactory()
request = factory.get(chapter_url)
expected = ([{'active': True, 'sections':
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
'format': u'Lecture Sequence', 'due': '', 'active': False},
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
'format': '', 'due': '', 'active': False},
{'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True,
'format': '', 'due': '', 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True,
'format': '', 'due': '', 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections':
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
'format': '', 'due': '', 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, None)
self.assertEqual(expected, actual)
def test_toc_toy_from_section(self):
chapter = 'Overview'
chapter_url = '%s/%s/%s' % ('/courses', self.course_name, chapter)
section = 'Welcome'
factory = RequestFactory()
request = factory.get(chapter_url)
expected = ([{'active': True, 'sections':
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
'format': u'Lecture Sequence', 'due': '', 'active': False},
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
'format': '', 'due': '', 'active': True},
{'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True,
'format': '', 'due': '', 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True,
'format': '', 'due': '', 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections':
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
'format': '', 'due': '', 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, section)
self.assertEqual(expected, actual)
from django.test import TestCase
from courseware import progress
from mock import MagicMock
class ProgessTests(TestCase):
def setUp(self):
self.d = dict({'duration_total': 0,
'duration_watched': 0,
'done': True,
'questions_correct': 4,
'questions_incorrect': 0,
'questions_total': 0})
self.c = progress.completion()
self.c2 = progress.completion()
self.c2.dict = dict({'duration_total': 0,
'duration_watched': 0,
'done': True,
'questions_correct': 2,
'questions_incorrect': 1,
'questions_total': 0})
self.cplusc2 = dict({'duration_total': 0,
'duration_watched': 0,
'done': True,
'questions_correct': 2,
'questions_incorrect': 1,
'questions_total': 0})
self.oth = dict({'duration_total': 0,
'duration_watched': 0,
'done': True,
'questions_correct': 4,
'questions_incorrect': 0,
'questions_total': 7})
self.x = MagicMock()
self.x.dict = self.oth
self.d_oth = {'duration_total': 0,
'duration_watched': 0,
'done': True,
'questions_correct': 4,
'questions_incorrect': 0,
'questions_total': 7}
def test_getitem(self):
self.assertEqual(self.c.__getitem__('duration_watched'), 0)
def test_setitem(self):
self.c.__setitem__('questions_correct', 4)
self.assertEqual(str(self.c), str(self.d))
def test_repr(self):
self.assertEqual(self.c.__repr__(), str(progress.completion()))
import logging
from mock import MagicMock, patch
import datetime
import factory
import unittest
import os
from django.test import TestCase
from django.http import Http404, HttpResponse
from django.conf import settings
from django.test.utils import override_settings
from django.contrib.auth.models import User
from django.test.client import RequestFactory
from student.models import CourseEnrollment
from xmodule.modulestore.django import modulestore, _MODULESTORES
from xmodule.modulestore.exceptions import InvalidLocationError,\
ItemNotFoundError, NoPathToItem
import courseware.views as views
from xmodule.modulestore import Location
from factories import UserFactory
class Stub():
pass
# This part is required for modulestore() to work properly
def xml_store_config(data_dir):
return {
'default': {
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
'OPTIONS': {
'data_dir': data_dir,
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
}
}
}
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestJumpTo(TestCase):
"""Check the jumpto link for a course"""
def setUp(self):
self._MODULESTORES = {}
# Toy courses should be loaded
self.course_name = 'edX/toy/2012_Fall'
self.toy_course = modulestore().get_course('edX/toy/2012_Fall')
def test_jumpto_invalid_location(self):
location = Location('i4x', 'edX', 'toy', 'NoSuchPlace', None)
jumpto_url = '%s/%s/jump_to/%s' % ('/courses', self.course_name, location)
expected = 'courses/edX/toy/2012_Fall/courseware/Overview/'
response = self.client.get(jumpto_url)
self.assertEqual(response.status_code, 404)
def test_jumpto_from_chapter(self):
location = Location('i4x', 'edX', 'toy', 'chapter', 'Overview')
jumpto_url = '%s/%s/jump_to/%s' % ('/courses', self.course_name, location)
expected = 'courses/edX/toy/2012_Fall/courseware/Overview/'
response = self.client.get(jumpto_url)
self.assertRedirects(response, expected, status_code=302, target_status_code=302)
class ViewsTestCase(TestCase):
def setUp(self):
self.user = User.objects.create(username='dummy', password='123456',
email='test@mit.edu')
self.date = datetime.datetime(2013, 1, 22)
self.course_id = 'edX/toy/2012_Fall'
self.enrollment = CourseEnrollment.objects.get_or_create(user=self.user,
course_id=self.course_id,
created=self.date)[0]
self.location = ['tag', 'org', 'course', 'category', 'name']
self._MODULESTORES = {}
# This is a CourseDescriptor object
self.toy_course = modulestore().get_course('edX/toy/2012_Fall')
self.request_factory = RequestFactory()
chapter = 'Overview'
self.chapter_url = '%s/%s/%s' % ('/courses', self.course_id, chapter)
def test_user_groups(self):
# depreciated function
mock_user = MagicMock()
mock_user.is_authenticated.return_value = False
self.assertEquals(views.user_groups(mock_user), [])
def test_get_current_child(self):
self.assertIsNone(views.get_current_child(Stub()))
mock_xmodule = MagicMock()
mock_xmodule.position = -1
mock_xmodule.get_display_items.return_value = ['one', 'two']
self.assertEquals(views.get_current_child(mock_xmodule), 'one')
mock_xmodule_2 = MagicMock()
mock_xmodule_2.position = 3
mock_xmodule_2.get_display_items.return_value = []
self.assertIsNone(views.get_current_child(mock_xmodule_2))
def test_redirect_to_course_position(self):
mock_module = MagicMock()
mock_module.descriptor.id = 'Underwater Basketweaving'
mock_module.position = 3
mock_module.get_display_items.return_value = []
self.assertRaises(Http404, views.redirect_to_course_position,
mock_module, True)
def test_registered_for_course(self):
self.assertFalse(views.registered_for_course('Basketweaving', None))
mock_user = MagicMock()
mock_user.is_authenticated.return_value = False
self.assertFalse(views.registered_for_course('dummy', mock_user))
mock_course = MagicMock()
mock_course.id = self.course_id
self.assertTrue(views.registered_for_course(mock_course, self.user))
def test_jump_to_invalid(self):
request = self.request_factory.get(self.chapter_url)
self.assertRaisesRegexp(Http404, 'Invalid location', views.jump_to,
request, 'bar', ())
self.assertRaisesRegexp(Http404, 'No data*', views.jump_to, request,
'dummy', self.location)
......@@ -64,7 +64,7 @@ def mongo_store_config(data_dir):
'render_template': 'mitxmako.shortcuts.render_to_string',
}
}
}
}
def draft_mongo_store_config(data_dir):
......@@ -80,7 +80,7 @@ def draft_mongo_store_config(data_dir):
'render_template': 'mitxmako.shortcuts.render_to_string',
}
}
}
}
def xml_store_config(data_dir):
......@@ -92,7 +92,7 @@ def xml_store_config(data_dir):
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
}
}
}
}
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
......@@ -115,8 +115,7 @@ class ActivateLoginTestCase(TestCase):
'Response status code was {0} instead of 302'.format(response.status_code))
url = response['Location']
e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(
expected_url)
e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(expected_url)
if not (e_scheme or e_netloc):
expected_url = urlunsplit(('http', 'testserver', e_path,
e_query, e_fragment))
......@@ -234,7 +233,6 @@ class PageLoader(ActivateLoginTestCase):
data = parse_json(resp)
self.assertTrue(data['success'])
def check_for_get_code(self, code, url):
"""
Check that we got the expected code when accessing url via GET.
......@@ -246,7 +244,6 @@ class PageLoader(ActivateLoginTestCase):
.format(resp.status_code, url, code))
return resp
def check_for_post_code(self, code, url, data={}):
"""
Check that we got the expected code when accessing url via POST.
......@@ -258,12 +255,8 @@ class PageLoader(ActivateLoginTestCase):
.format(resp.status_code, url, code))
return resp
def check_pages_load(self, module_store):
"""Make all locations in course load"""
# enroll in the course before trying to access pages
courses = module_store.get_courses()
self.assertEqual(len(courses), 1)
......@@ -344,7 +337,6 @@ class PageLoader(ActivateLoginTestCase):
self.assertTrue(all_ok)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestCoursesLoadTestCase_XmlModulestore(PageLoader):
'''Check that all pages in test courses load properly'''
......@@ -525,7 +517,6 @@ class TestViewAuth(PageLoader):
print 'checking for 404 on {0}'.format(url)
self.check_for_get_code(404, url)
# now also make the instructor staff
u = user(self.instructor)
u.is_staff = True
......@@ -536,7 +527,6 @@ class TestViewAuth(PageLoader):
print 'checking for 200 on {0}'.format(url)
self.check_for_get_code(200, url)
def run_wrapped(self, test):
"""
test.py turns off start dates. Enable them.
......@@ -552,7 +542,6 @@ class TestViewAuth(PageLoader):
finally:
settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD
def test_dark_launch(self):
"""Make sure that before course start, students can't access course
pages, but instructors can"""
......@@ -646,7 +635,6 @@ class TestViewAuth(PageLoader):
url = reverse_urls(['courseware'], course)[0]
self.check_for_get_code(302, url)
# First, try with an enrolled student
print '=== Testing student access....'
self.login(self.student, self.password)
......@@ -761,7 +749,6 @@ class TestViewAuth(PageLoader):
self.assertTrue(has_access(student_user, self.toy, 'load'))
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestCourseGrader(PageLoader):
"""Check that a course gets graded properly"""
......@@ -832,8 +819,7 @@ class TestCourseGrader(PageLoader):
kwargs={
'course_id': self.graded_course.id,
'location': problem_location,
'dispatch': 'problem_check', }
)
'dispatch': 'problem_check', })
resp = self.client.post(modx_url, {
'input_i4x-edX-graded-problem-{0}_2_1'.format(problem_url_name): responses[0],
......@@ -854,8 +840,7 @@ class TestCourseGrader(PageLoader):
kwargs={
'course_id': self.graded_course.id,
'location': problem_location,
'dispatch': 'problem_reset', }
)
'dispatch': 'problem_reset', })
resp = self.client.post(modx_url)
return resp
......
......@@ -549,11 +549,6 @@ def instructor_dashboard(request, course_id):
msg += "Error! Failed to un-enroll student with email '%s'\n" % student
msg += str(err) + '\n'
elif action == 'Un-enroll ALL students':
ret = _do_enroll_students(course, course_id, '', overload=True)
datatable = ret['datatable']
elif action == 'Enroll multiple students':
students = request.POST.get('enroll_multiple', '')
......
......@@ -89,14 +89,24 @@ def peer_grading(request, course_id):
Show a peer grading interface
'''
#Get the current course
course = get_course_with_access(request.user, course_id, 'load')
course_id_parts = course.id.split("/")
course_id_norun = "/".join(course_id_parts[0:2])
pg_location = "i4x://" + course_id_norun + "/peergrading/init"
false_dict = [False,"False", "false", "FALSE"]
#Reverse the base course url
base_course_url = reverse('courses')
try:
problem_url_parts = search.path_to_location(modulestore(), course.id, pg_location)
#TODO: This will not work with multiple runs of a course. Make it work. The last key in the Location passed
#to get_items is called revision. Is this the same as run?
#Get the peer grading modules currently in the course
items = modulestore().get_items(['i4x', None, course_id_parts[1], 'peergrading', None])
#See if any of the modules are centralized modules (ie display info from multiple problems)
items = [i for i in items if i.metadata.get("use_for_single_location", True) in false_dict]
#Get the first one
item_location = items[0].location
#Generate a url for the first module and redirect the user to it
problem_url_parts = search.path_to_location(modulestore(), course.id, item_location)
problem_url = generate_problem_url(problem_url_parts, base_course_url)
return HttpResponseRedirect(problem_url)
......
......@@ -246,7 +246,6 @@ function goto( mode)
<p>
Student Email: <input type="text" name="enstudent"> <input type="submit" name="action" value="Un-enroll student">
<input type="submit" name="action" value="Enroll student">
<input type="submit" name="action" value="Un-enroll ALL students">
<hr width="40%" style="align:left">
%if settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access:
......
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