Merge remote-tracking branch 'origin/master' into will/combine-reg-login-form
Conflicts: lms/static/sass/base/_grid-settings.scss lms/static/sass/shared/_footer.scss lms/static/sass/shared/_header.scss
This commit is contained in:
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -333,7 +333,6 @@ def get_codemirror_value(index=0, find_prefix="$"):
|
||||
)
|
||||
|
||||
|
||||
|
||||
def attach_file(filename, sub_path):
|
||||
path = os.path.join(TEST_ROOT, sub_path, filename)
|
||||
world.browser.execute_script("$('input.file-input').css('display', 'block')")
|
||||
@@ -388,4 +387,3 @@ def create_other_user(_step, name, has_extra_perms, role_name):
|
||||
@step('I log out')
|
||||
def log_out(_step):
|
||||
world.visit('logout')
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# pylint: disable=C0111
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true # pylint: disable=E0611
|
||||
from nose.tools import assert_true # pylint: disable=E0611
|
||||
from video_editor import RequestHandlerWithSessionId, success_upload_file
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '''Delete a MongoDB backed course'''
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class Command(BaseCommand):
|
||||
try:
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0])
|
||||
except InvalidKeyError:
|
||||
raise CommandError(GitExportError.BAD_COURSE)
|
||||
raise CommandError(_(GitExportError.BAD_COURSE))
|
||||
|
||||
try:
|
||||
git_export_utils.export_to_git(
|
||||
@@ -72,4 +72,4 @@ class Command(BaseCommand):
|
||||
options.get('rdir', None)
|
||||
)
|
||||
except git_export_utils.GitExportError as ex:
|
||||
raise CommandError(str(ex))
|
||||
raise CommandError(_(ex.message))
|
||||
|
||||
@@ -11,6 +11,8 @@ from django.db.utils import IntegrityError
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole
|
||||
|
||||
#------------ to run: ./manage.py cms populate_creators --settings=dev
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Script for granting existing course instructors course creator privileges.
|
||||
|
||||
@@ -11,8 +11,13 @@ def query_yes_no(question, default="yes"):
|
||||
|
||||
The "answer" return value is one of "yes" or "no".
|
||||
"""
|
||||
valid = {"yes": True, "y": True, "ye": True,
|
||||
"no": False, "n": False}
|
||||
valid = {
|
||||
"yes": True,
|
||||
"y": True,
|
||||
"ye": True,
|
||||
"no": False,
|
||||
"n": False,
|
||||
}
|
||||
if default is None:
|
||||
prompt = " [y/n] "
|
||||
elif default == "yes":
|
||||
|
||||
@@ -10,4 +10,3 @@ class Command(BaseCommand):
|
||||
raise CommandError("restore_asset_from_trashcan requires one argument: <location>")
|
||||
|
||||
restore_asset_from_trashcan(args[0])
|
||||
|
||||
|
||||
@@ -80,6 +80,34 @@ class TestGitExport(CourseTestCase):
|
||||
stderr=StringIO.StringIO())
|
||||
self.assertEqual(ex.exception.code, 1)
|
||||
|
||||
def test_error_output(self):
|
||||
"""
|
||||
Verify that error output is actually resolved as the correct string
|
||||
"""
|
||||
output = StringIO.StringIO()
|
||||
with self.assertRaises(SystemExit):
|
||||
with self.assertRaisesRegexp(CommandError, GitExportError.BAD_COURSE):
|
||||
call_command(
|
||||
'git_export', 'foo/bar:baz', 'silly',
|
||||
stdout=output, stderr=output
|
||||
)
|
||||
self.assertIn('Bad course location provided', output.getvalue())
|
||||
output.close()
|
||||
|
||||
output = StringIO.StringIO()
|
||||
with self.assertRaises(SystemExit):
|
||||
with self.assertRaisesRegexp(CommandError, GitExportError.URL_BAD):
|
||||
call_command(
|
||||
'git_export', 'foo/bar/baz', 'silly',
|
||||
stdout=output, stderr=output
|
||||
)
|
||||
self.assertIn(
|
||||
'Non writable git url provided. Expecting something like:'
|
||||
' git@github.com:mitocw/edx4edx_lite.git',
|
||||
output.getvalue()
|
||||
)
|
||||
output.close()
|
||||
|
||||
def test_bad_git_url(self):
|
||||
"""
|
||||
Test several bad URLs for validation
|
||||
|
||||
@@ -161,7 +161,6 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
|
||||
number = '999'
|
||||
display_name = 'Test course'
|
||||
|
||||
|
||||
def clear_sub_content(self, subs_id):
|
||||
"""
|
||||
Remove, if subtitle content exists.
|
||||
@@ -472,6 +471,7 @@ class TestYoutubeTranscripts(unittest.TestCase):
|
||||
self.assertEqual(transcripts, expected_transcripts)
|
||||
mock_get.assert_called_with('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_youtube_id'})
|
||||
|
||||
|
||||
class TestTranscript(unittest.TestCase):
|
||||
"""
|
||||
Tests for Transcript class e.g. different transcript conversions.
|
||||
@@ -489,7 +489,6 @@ class TestTranscript(unittest.TestCase):
|
||||
|
||||
""")
|
||||
|
||||
|
||||
self.sjson_transcript = textwrap.dedent("""\
|
||||
{
|
||||
"start": [
|
||||
|
||||
@@ -86,6 +86,7 @@ class LMSLinksTestCase(TestCase):
|
||||
link = utils.get_lms_link_for_item(location)
|
||||
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/course/test")
|
||||
|
||||
|
||||
class ExtraPanelTabTestCase(TestCase):
|
||||
""" Tests adding and removing extra course tabs. """
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ def expand_checklist_action_url(course_module, checklist):
|
||||
|
||||
return expanded_checklist
|
||||
|
||||
|
||||
def localize_checklist_text(checklist):
|
||||
"""
|
||||
Localize texts for a given checklist and returns the modified version.
|
||||
|
||||
@@ -261,6 +261,7 @@ def course_rerun_handler(request, course_key_string):
|
||||
'allow_unicode_course_id': settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID', False)
|
||||
})
|
||||
|
||||
|
||||
def _course_outline_json(request, course_module):
|
||||
"""
|
||||
Returns a JSON representation of the course module and recursively all of its children.
|
||||
|
||||
@@ -151,7 +151,7 @@ def _preview_module_system(request, descriptor):
|
||||
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id),
|
||||
user=request.user,
|
||||
can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
|
||||
get_python_lib_zip=(lambda :get_python_lib_zip(contentstore, course_id)),
|
||||
get_python_lib_zip=(lambda: get_python_lib_zip(contentstore, course_id)),
|
||||
mixins=settings.XBLOCK_MIXINS,
|
||||
course_id=course_id,
|
||||
anonymous_student_id='student',
|
||||
|
||||
@@ -20,6 +20,7 @@ from ..utils import get_lms_link_for_item
|
||||
|
||||
__all__ = ['tabs_handler']
|
||||
|
||||
|
||||
@expect_json
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@@ -203,4 +204,3 @@ def primitive_insert(course, num, tab_type, name):
|
||||
tabs = course.tabs
|
||||
tabs.insert(num, new_tab)
|
||||
modulestore().update_item(course, ModuleStoreEnum.UserID.primitive_command)
|
||||
|
||||
|
||||
@@ -377,7 +377,10 @@ def choose_transcripts(request):
|
||||
if item.sub != html5_id: # update sub value
|
||||
item.sub = html5_id
|
||||
item.save_with_metadata(request.user)
|
||||
response = {'status': 'Success', 'subs': item.sub}
|
||||
response = {
|
||||
'status': 'Success',
|
||||
'subs': item.sub,
|
||||
}
|
||||
return JsonResponse(response)
|
||||
|
||||
|
||||
@@ -408,7 +411,10 @@ def replace_transcripts(request):
|
||||
|
||||
item.sub = youtube_id
|
||||
item.save_with_metadata(request.user)
|
||||
response = {'status': 'Success', 'subs': item.sub}
|
||||
response = {
|
||||
'status': 'Success',
|
||||
'subs': item.sub,
|
||||
}
|
||||
return JsonResponse(response)
|
||||
|
||||
|
||||
|
||||
@@ -100,7 +100,6 @@ def _course_team_user(request, course_key, email):
|
||||
}
|
||||
return JsonResponse(msg, 400)
|
||||
|
||||
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
except Exception:
|
||||
|
||||
@@ -11,6 +11,7 @@ from models.settings import course_grading
|
||||
from xmodule.fields import Date
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
class CourseDetails(object):
|
||||
def __init__(self, org, course_id, run):
|
||||
# still need these for now b/c the client's screen shows these 3 fields
|
||||
|
||||
@@ -736,7 +736,7 @@ ADVANCED_COMPONENT_TYPES = [
|
||||
'done', # Lets students mark things as done. See https://github.com/pmitros/DoneXBlock
|
||||
'audio', # Embed an audio file. See https://github.com/pmitros/AudioXBlock
|
||||
'recommender', # Crowdsourced recommender. Prototype by dli&pmitros. Intended for roll-out in one place in one course.
|
||||
'profile', # Prototype user profile XBlock. Used to test XBlock parameter passing. See https://github.com/pmitros/ProfileXBlock
|
||||
'profile', # Prototype user profile XBlock. Used to test XBlock parameter passing. See https://github.com/pmitros/ProfileXBlock
|
||||
'split_test',
|
||||
'combinedopenended',
|
||||
'peergrading',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Specific overrides to the base prod settings to make development easier.
|
||||
"""
|
||||
|
||||
from .aws import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .aws import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
# Don't use S3 in devstack, fall back to filesystem
|
||||
del DEFAULT_FILE_STORAGE
|
||||
|
||||
@@ -69,9 +69,9 @@ STATICFILES_DIRS += [
|
||||
# If we don't add these settings, then Django templates that can't
|
||||
# find pipelined assets will raise a ValueError.
|
||||
# http://stackoverflow.com/questions/12816941/unit-testing-with-django-pipeline
|
||||
STATICFILES_STORAGE='pipeline.storage.NonPackagingPipelineStorage'
|
||||
STATICFILES_STORAGE = 'pipeline.storage.NonPackagingPipelineStorage'
|
||||
STATIC_URL = "/static/"
|
||||
PIPELINE_ENABLED=False
|
||||
PIPELINE_ENABLED = False
|
||||
|
||||
# Update module store settings per defaults for tests
|
||||
update_module_store_settings(
|
||||
|
||||
@@ -45,7 +45,8 @@
|
||||
'js/factories/settings',
|
||||
'js/factories/settings_advanced',
|
||||
'js/factories/settings_graders',
|
||||
'js/factories/textbooks'
|
||||
'js/factories/textbooks',
|
||||
'js/factories/xblock_validation'
|
||||
]),
|
||||
/**
|
||||
* By default all the configuration for optimization happens from the command
|
||||
|
||||
@@ -244,6 +244,8 @@ define([
|
||||
"js/spec/views/modals/edit_xblock_spec",
|
||||
"js/spec/views/modals/validation_error_modal_spec",
|
||||
|
||||
"js/spec/factories/xblock_validation_spec",
|
||||
|
||||
"js/spec/xblock/cms.runtime.v1_spec",
|
||||
|
||||
# these tests are run separately in the cms-squire suite, due to process
|
||||
|
||||
15
cms/static/js/factories/xblock_validation.js
Normal file
15
cms/static/js/factories/xblock_validation.js
Normal file
@@ -0,0 +1,15 @@
|
||||
define(["js/views/xblock_validation", "js/models/xblock_validation"],
|
||||
function (XBlockValidationView, XBlockValidationModel) {
|
||||
'use strict';
|
||||
return function (validationMessages, hasEditingUrl, isRoot, validationEle) {
|
||||
if (hasEditingUrl && !isRoot) {
|
||||
validationMessages.showSummaryOnly = true;
|
||||
}
|
||||
|
||||
var model = new XBlockValidationModel(validationMessages, {parse: true});
|
||||
|
||||
if (!model.get("empty")) {
|
||||
new XBlockValidationView({el: validationEle, model: model, root: isRoot}).render();
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -18,10 +18,10 @@ define(["backbone", "gettext", "underscore"], function (Backbone, gettext, _) {
|
||||
if (!response.empty) {
|
||||
var summary = "summary" in response ? response.summary : {};
|
||||
var messages = "messages" in response ? response.messages : [];
|
||||
if (!(_.has(summary, "text")) || !summary.text) {
|
||||
if (!summary.text) {
|
||||
summary.text = gettext("This component has validation issues.");
|
||||
}
|
||||
if (!(_.has(summary, "type")) || !summary.type) {
|
||||
if (!summary.type) {
|
||||
summary.type = this.WARNING;
|
||||
// Possible types are ERROR, WARNING, and NOT_CONFIGURED. NOT_CONFIGURED is treated as a warning.
|
||||
_.find(messages, function (message) {
|
||||
|
||||
78
cms/static/js/spec/factories/xblock_validation_spec.js
Normal file
78
cms/static/js/spec/factories/xblock_validation_spec.js
Normal file
@@ -0,0 +1,78 @@
|
||||
define(['jquery', 'js/factories/xblock_validation', 'js/common_helpers/template_helpers'],
|
||||
function($, XBlockValidationFactory, TemplateHelpers) {
|
||||
|
||||
describe('XBlockValidationFactory', function() {
|
||||
var messageDiv;
|
||||
|
||||
beforeEach(function () {
|
||||
TemplateHelpers.installTemplate('xblock-validation-messages');
|
||||
appendSetFixtures($('<div class="messages"></div>'));
|
||||
messageDiv = $('.messages');
|
||||
});
|
||||
|
||||
it('Does not attach a view if messages is empty', function() {
|
||||
XBlockValidationFactory({"empty": true}, false, false, messageDiv);
|
||||
expect(messageDiv.children().length).toEqual(0);
|
||||
});
|
||||
|
||||
it('Does attach a view if messages are not empty', function() {
|
||||
XBlockValidationFactory({"empty": false}, false, false, messageDiv);
|
||||
expect(messageDiv.children().length).toEqual(1);
|
||||
});
|
||||
|
||||
it('Passes through the root property to the view.', function() {
|
||||
var noContainerContent = "no-container-content";
|
||||
|
||||
var notConfiguredMessages = {
|
||||
"empty": false,
|
||||
"summary": {"text": "my summary", "type": "not-configured"},
|
||||
"messages": [],
|
||||
"xblock_id": "id"
|
||||
};
|
||||
// Root is false, will not add noContainerContent.
|
||||
XBlockValidationFactory(notConfiguredMessages, true, false, messageDiv);
|
||||
expect(messageDiv.find('.validation')).not.toHaveClass(noContainerContent);
|
||||
|
||||
// Root is true, will add noContainerContent.
|
||||
XBlockValidationFactory(notConfiguredMessages, true, true, messageDiv);
|
||||
expect(messageDiv.find('.validation')).toHaveClass(noContainerContent);
|
||||
});
|
||||
|
||||
describe('Controls display of detailed messages based on url and root property', function() {
|
||||
var messagesWithSummary, checkDetailedMessages;
|
||||
|
||||
beforeEach(function () {
|
||||
messagesWithSummary = {
|
||||
"empty": false,
|
||||
"summary": {"text": "my summary"},
|
||||
"messages": [{"text": "one", "type": "warning"}, {"text": "two", "type": "error"}],
|
||||
"xblock_id": "id"
|
||||
};
|
||||
});
|
||||
|
||||
checkDetailedMessages = function (expectedDetailedMessages) {
|
||||
expect(messageDiv.children().length).toEqual(1);
|
||||
expect(messageDiv.find('.xblock-message-item').length).toBe(expectedDetailedMessages);
|
||||
};
|
||||
|
||||
it('Does not show details if xblock has an editing URL and it is not rendered as root', function() {
|
||||
XBlockValidationFactory(messagesWithSummary, true, false, messageDiv);
|
||||
checkDetailedMessages(0);
|
||||
});
|
||||
|
||||
it('Shows details if xblock does not have its own editing URL, regardless of root value', function() {
|
||||
XBlockValidationFactory(messagesWithSummary, false, false, messageDiv);
|
||||
checkDetailedMessages(2);
|
||||
|
||||
XBlockValidationFactory(messagesWithSummary, false, true, messageDiv);
|
||||
checkDetailedMessages(2);
|
||||
});
|
||||
|
||||
it('Shows details if xblock has its own editing URL and is rendered as root', function() {
|
||||
XBlockValidationFactory(messagesWithSummary, true, true, messageDiv);
|
||||
checkDetailedMessages(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("Adding Files for Your Course")}</h3>
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title title-3">${_("What are course checklists?")}</h3>
|
||||
<p>
|
||||
|
||||
@@ -106,7 +106,7 @@ templates = ["basic-modal", "modal-button", "edit-xblock-modal",
|
||||
<p><span class="spin"><i class="icon-refresh"></i></span> <span class="copy">${_("Loading...")}</span></p>
|
||||
</div>
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
% if not is_unit_page:
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("Adding components")}</h3>
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("When will my course re-run start?")}</h3>
|
||||
<ul class="list-details">
|
||||
|
||||
@@ -114,7 +114,7 @@ from contentstore.utils import reverse_usage_url
|
||||
<p><span class="spin"><i class="icon-refresh"></i></span> <span class="copy">${_("Loading...")}</span></p>
|
||||
</div>
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("Creating your course organization")}</h3>
|
||||
<p>${_("You add sections, subsections, and units directly in the outline.")}</p>
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("What are pages?")}</h3>
|
||||
<p>${_("Pages are listed horizontally at the top of your course. Default pages (Courseware, Course info, Discussion, Wiki, and Progress) are followed by textbooks and custom pages that you create.")}</p>
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("Why export a course?")}</h3>
|
||||
<p>${_("You may want to edit the XML in your course directly, outside of Studio. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.")}</p>
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
% endif
|
||||
</div>
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<dl class="export-git-info-block">
|
||||
<dt>${_("Your course:")}</dt>
|
||||
<dd class="course_text">${context_course.id | h}</dd>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
% endif
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("What can I do on this page?")}</h3>
|
||||
<p>${_("You can create, edit, and delete group configurations.")}</p>
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("Why import a course?")}</h3>
|
||||
<p>${_("You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside Studio.")}</p>
|
||||
|
||||
@@ -357,7 +357,7 @@
|
||||
% endif
|
||||
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title title-3">${_('New to edX Studio?')}</h3>
|
||||
<p>${_('Click Help in the upper-right corner to get more information about the Studio page you are viewing. You can also use the links at the bottom of the page to access our continously updated documentation and other Studio resources.')}</p>
|
||||
@@ -422,7 +422,7 @@
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title title-3">${_('Need help?')}</h3>
|
||||
<p>${_('Please check your Junk or Spam folders in case our email isn\'t in your INBOX. Still can\'t find the verification email? Request help via the link below.')}</p>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<p><span class="spin"><i class="icon-refresh"></i></span> <span class="copy">Loading...</span></p>
|
||||
</div>
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div id="publish-unit" class="window"></div>
|
||||
<div id="publish-history"></div>
|
||||
</aside>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<p><span class="spin"><i class="icon-refresh"></i></span> <span class="copy">Loading...</span></p>
|
||||
</div>
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">What can I do on this page?</h3>
|
||||
<p>You can create new sections and subsections, set the release date for sections, and create new units in existing subsections. You can set the assignment type for subsections that are to be graded, and you can open a subsection for further editing.</p>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<p><span class="spin"><i class="icon-refresh"></i></span> <span class="copy">${_("Loading...")}</span></p>
|
||||
</div>
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complimentary"></aside>
|
||||
<aside class="content-supplementary" role="complementary"></aside>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,7 @@ from django.utils.translation import ugettext as _
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<h2 class="sr">${_("Studio Support")}</h2>
|
||||
|
||||
<div class="bit">
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
%endif
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("Course Team Roles")}</h3>
|
||||
<p>${_("Course team members, or staff, are course co-authors. They have full writing and editing privileges on all course content.")}</p>
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<h2 class="sr">${_("Common Studio Questions")}</h2>
|
||||
|
||||
<div class="bit">
|
||||
|
||||
@@ -292,7 +292,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
|
||||
% endif
|
||||
</form>
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("How are these settings used?")}</h3>
|
||||
<p>${_("Your course's schedule determines when students can enroll in and begin a course.")}</p>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("What do advanced settings do?")}</h3>
|
||||
<p>${_("Advanced settings control specific course functionality. On this page, you can edit manual policies, which are JSON-based key and value pairs that control specific course settings.")}</p>
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("What can I do on this page?")}</h3>
|
||||
<p>${_("You can use the slider under Overall Grade Range to specify whether your course is pass/fail or graded by letter, and to establish the thresholds for each grade.")}</p>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from contentstore.views.helpers import xblock_studio_url
|
||||
@@ -21,31 +20,17 @@ messages = json.dumps(xblock.validate().to_json())
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<script type='text/javascript'>
|
||||
require(["js/views/xblock_validation", "js/models/xblock_validation"],
|
||||
function (XBlockValidationView, XBlockValidationModel) {
|
||||
var validationMessages = ${messages};
|
||||
% if xblock_url and not is_root:
|
||||
validationMessages.showSummaryOnly = true;
|
||||
% endif
|
||||
|
||||
var model = new XBlockValidationModel(validationMessages, {parse: true});
|
||||
if (!model.get("empty")) {
|
||||
var validationEle = $('div.xblock-validation-messages[data-locator="${xblock.location | h}"]');
|
||||
var viewOptions = {
|
||||
el: validationEle,
|
||||
model: model
|
||||
};
|
||||
% if is_root:
|
||||
viewOptions.root = true;
|
||||
% endif
|
||||
var view = new XBlockValidationView(viewOptions);
|
||||
view.render();
|
||||
}
|
||||
});
|
||||
<script>
|
||||
require(["jquery", "js/factories/xblock_validation"], function($, XBlockValidationFactory) {
|
||||
XBlockValidationFactory(
|
||||
${messages},
|
||||
$.parseJSON("${bool(xblock_url)}".toLowerCase()), // xblock_url will be None or a string
|
||||
$.parseJSON("${bool(is_root)}".toLowerCase()), // is_root will be None or a boolean
|
||||
$('div.xblock-validation-messages[data-locator="${xblock.location | h}"]')
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
% if not is_root:
|
||||
% if is_reorderable:
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="${xblock.location | h}" data-course-key="${xblock.location.course_key | h}">
|
||||
|
||||
@@ -54,7 +54,7 @@ CMS.URL.LMS_BASE = "${settings.LMS_BASE}"
|
||||
<article class="content-primary" role="main">
|
||||
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("Why should I break my textbook into chapters?")}</h3>
|
||||
<p>${_("Breaking your textbook into multiple chapters reduces loading times for students, especially those with slow Internet connections. Breaking up textbooks into chapters can also help students more easily find topic-based information.")}</p>
|
||||
|
||||
@@ -455,7 +455,7 @@
|
||||
</article>
|
||||
</section>
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
|
||||
<!-- begin publishing changes 1 -->
|
||||
|
||||
|
||||
@@ -325,7 +325,7 @@
|
||||
|
||||
</article><!-- /content-primary -->
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">When will my course re-run start?</h3>
|
||||
<ul class="list-details">
|
||||
|
||||
@@ -757,7 +757,7 @@ from django.core.urlresolvers import reverse
|
||||
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">What can I do on this page?</h3>
|
||||
<p>You can create new sections and subsections, set the release date for sections, and create new units in existing subsections. You can set the assignment type for subsections that are to be graded, and you can open a subsection for further editing.</p>
|
||||
|
||||
@@ -17,4 +17,3 @@ startup.run()
|
||||
# as well as any WSGI server configured to use this file.
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
application = get_wsgi_application()
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from xmodule.exceptions import NotFoundError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StaticContentServer(object):
|
||||
def process_request(self, request):
|
||||
# look to see if the request is prefixed with an asset prefix tag
|
||||
|
||||
@@ -322,6 +322,7 @@ def add_cohort(course_key, name):
|
||||
)
|
||||
return cohort
|
||||
|
||||
|
||||
def add_user_to_cohort(cohort, username_or_email):
|
||||
"""
|
||||
Look up the given user, and if successful, add them to the specified cohort.
|
||||
|
||||
@@ -209,6 +209,7 @@ class ListCohortsTestCase(CohortViewsTestCase):
|
||||
actual_cohorts,
|
||||
)
|
||||
|
||||
|
||||
class AddCohortTestCase(CohortViewsTestCase):
|
||||
"""
|
||||
Tests the `add_cohort` view.
|
||||
|
||||
@@ -151,7 +151,6 @@ class CourseModeViewTest(ModuleStoreTestCase):
|
||||
response = self.client.get(choose_track_url)
|
||||
self.assertRedirects(response, reverse('dashboard'))
|
||||
|
||||
|
||||
# Mapping of course modes to the POST parameters sent
|
||||
# when the user chooses that mode.
|
||||
POST_PARAMS_FOR_COURSE_MODE = {
|
||||
|
||||
@@ -94,6 +94,7 @@ class ChooseModeView(View):
|
||||
"error": error,
|
||||
"upgrade": upgrade,
|
||||
"can_audit": "audit" in modes,
|
||||
"responsive": True
|
||||
}
|
||||
if "verified" in modes:
|
||||
context["suggested_prices"] = [
|
||||
|
||||
@@ -144,7 +144,6 @@ class DarkLangMiddlewareTests(TestCase):
|
||||
self.process_request(accept='rel-ter;q=1.0, rel;q=0.5')
|
||||
)
|
||||
|
||||
|
||||
def assertSessionLangEquals(self, value, request):
|
||||
"""
|
||||
Assert that the 'django_language' set in request.session is equal to value
|
||||
|
||||
@@ -4,6 +4,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from django_comment_common.models import Role
|
||||
from student.models import CourseEnrollment, User
|
||||
|
||||
|
||||
class RoleAssignmentTest(TestCase):
|
||||
"""
|
||||
Basic checks to make sure our Roles get assigned and unassigned as students
|
||||
|
||||
@@ -34,7 +34,6 @@ class MakoLoader(object):
|
||||
|
||||
self.module_directory = module_directory
|
||||
|
||||
|
||||
def __call__(self, template_name, template_dirs=None):
|
||||
return self.load_template(template_name, template_dirs)
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ def clear_lookups(namespace):
|
||||
if namespace in LOOKUP:
|
||||
del LOOKUP[namespace]
|
||||
|
||||
|
||||
def add_lookup(namespace, directory, package=None, prepend=False):
|
||||
"""
|
||||
Adds a new mako template lookup directory to the given namespace.
|
||||
|
||||
@@ -76,6 +76,7 @@ def marketing_link_context_processor(request):
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def open_source_footer_context_processor(request):
|
||||
"""
|
||||
Checks the site name to determine whether to use the edX.org footer or the Open Source Footer.
|
||||
@@ -97,6 +98,7 @@ def microsite_footer_context_processor(request):
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def render_to_string(template_name, dictionary, context=None, namespace='main'):
|
||||
|
||||
# see if there is an override template defined in the microsite
|
||||
|
||||
@@ -19,6 +19,7 @@ from edxmako.shortcuts import (
|
||||
from student.tests.factories import UserFactory
|
||||
from util.testing import UrlResetMixin
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class ShortcutsTests(UrlResetMixin, TestCase):
|
||||
"""
|
||||
|
||||
@@ -28,6 +28,7 @@ from embargo.models import EmbargoedCourse, EmbargoedState, IPFilter
|
||||
# that disables the XML modulestore.
|
||||
MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, include_xml=False)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@override_settings(MODULESTORE=MODULESTORE_CONFIG)
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
|
||||
@@ -37,6 +37,7 @@ FEATURES_WITHOUT_SSL_AUTH['AUTH_USE_CERTIFICATES'] = False
|
||||
|
||||
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {})
|
||||
|
||||
|
||||
@override_settings(FEATURES=FEATURES_WITH_SSL_AUTH)
|
||||
class SSLClientTest(ModuleStoreTestCase):
|
||||
"""
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.db.utils import DatabaseError
|
||||
import mock
|
||||
from django.test.testcases import TestCase
|
||||
|
||||
|
||||
class HeartbeatTestCase(TestCase):
|
||||
"""
|
||||
Test the heartbeat
|
||||
|
||||
@@ -23,6 +23,7 @@ def page_title_breadcrumbs(*crumbs, **kwargs):
|
||||
else:
|
||||
return platform_name()
|
||||
|
||||
|
||||
@register.simple_tag(name="page_title_breadcrumbs", takes_context=True)
|
||||
def page_title_breadcrumbs_tag(context, *crumbs):
|
||||
"""
|
||||
@@ -42,7 +43,7 @@ def platform_name():
|
||||
|
||||
|
||||
@register.simple_tag(name="favicon_path")
|
||||
def favicon_path(default=getattr(settings,'FAVICON_PATH', 'images/favicon.ico')):
|
||||
def favicon_path(default=getattr(settings, 'FAVICON_PATH', 'images/favicon.ico')):
|
||||
"""
|
||||
Django template tag that outputs the configured favicon:
|
||||
{% favicon_path %}
|
||||
|
||||
@@ -78,6 +78,7 @@ def post_save_metrics(sender, **kwargs):
|
||||
tags = _database_tags(action, sender, kwargs)
|
||||
dog_stats_api.increment('edxapp.db.model', tags=tags)
|
||||
|
||||
|
||||
@receiver(post_delete, dispatch_uid='edxapp.monitoring.post_delete_metrics')
|
||||
def post_delete_metrics(sender, **kwargs):
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Register signal handlers
|
||||
import signals
|
||||
import exceptions
|
||||
import exceptions
|
||||
|
||||
@@ -3,11 +3,12 @@ import threading
|
||||
_request_cache_threadlocal = threading.local()
|
||||
_request_cache_threadlocal.data = {}
|
||||
|
||||
|
||||
class RequestCache(object):
|
||||
@classmethod
|
||||
def get_request_cache(cls):
|
||||
return _request_cache_threadlocal
|
||||
|
||||
|
||||
def clear_request_cache(self):
|
||||
_request_cache_threadlocal.data = {}
|
||||
|
||||
@@ -17,4 +18,4 @@ class RequestCache(object):
|
||||
|
||||
def process_response(self, request, response):
|
||||
self.clear_request_cache()
|
||||
return response
|
||||
return response
|
||||
|
||||
@@ -36,7 +36,6 @@ class TestStatus(TestCase):
|
||||
"edX/toy/2012_Fall" : "A toy story"
|
||||
}"""
|
||||
|
||||
|
||||
# json to use, expected results for course=None (e.g. homepage),
|
||||
# for toy course, for full course. Note that get_site_status_msg
|
||||
# is supposed to return global message even if course=None. The
|
||||
|
||||
@@ -83,4 +83,3 @@ def _check_caller_authority(caller, role):
|
||||
elif isinstance(role, CourseRole): # instructors can change the roles w/in their course
|
||||
if not has_access(caller, CourseInstructorRole(role.course_key)):
|
||||
raise PermissionDenied
|
||||
|
||||
|
||||
@@ -22,4 +22,4 @@ class PasswordResetFormNoActive(PasswordResetForm):
|
||||
if any((user.password == UNUSABLE_PASSWORD)
|
||||
for user in self.users_cache):
|
||||
raise forms.ValidationError(self.error_messages['unusable'])
|
||||
return email
|
||||
return email
|
||||
|
||||
@@ -65,4 +65,3 @@ class Command(BaseCommand):
|
||||
))
|
||||
except IOError:
|
||||
raise CommandError("Error writing to file: %s" % output_filename)
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ class Command(TrackedCommand):
|
||||
# Move the Student between the classes.
|
||||
mode = enrollment.mode
|
||||
old_is_active = enrollment.is_active
|
||||
CourseEnrollment.unenroll(user, source_key, emit_unenrollment_event=False)
|
||||
CourseEnrollment.unenroll(user, source_key, skip_refund=True)
|
||||
print(u"Unenrolled {} from {}".format(user.username, unicode(source_key)))
|
||||
|
||||
for dest_key in dest_keys:
|
||||
@@ -98,7 +98,7 @@ class Command(TrackedCommand):
|
||||
# Un-enroll from the new course if the user had un-enrolled
|
||||
# form the old course.
|
||||
if not old_is_active:
|
||||
new_enrollment.update_enrollment(is_active=False, emit_unenrollment_event=False)
|
||||
new_enrollment.update_enrollment(is_active=False, skip_refund=True)
|
||||
|
||||
if transfer_certificates:
|
||||
self._transfer_certificate_item(source_key, enrollment, user, dest_keys, new_enrollment)
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
Tests the transfer student management command
|
||||
"""
|
||||
from django.conf import settings
|
||||
from mock import patch, call
|
||||
from opaque_keys.edx import locator
|
||||
import unittest
|
||||
import ddt
|
||||
|
||||
from shoppingcart.models import Order, CertificateItem # pylint: disable=F0401
|
||||
from course_modes.models import CourseMode
|
||||
from student.management.commands import transfer_students
|
||||
from student.models import CourseEnrollment
|
||||
from student.models import CourseEnrollment, UNENROLL_DONE, EVENT_NAME_ENROLLMENT_DEACTIVATED, \
|
||||
EVENT_NAME_ENROLLMENT_ACTIVATED, EVENT_NAME_ENROLLMENT_MODE_CHANGED
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@@ -18,18 +23,40 @@ class TestTransferStudents(ModuleStoreTestCase):
|
||||
"""Tests for transferring students between courses."""
|
||||
|
||||
PASSWORD = 'test'
|
||||
signal_fired = False
|
||||
|
||||
def setUp(self, **kwargs):
|
||||
"""Connect a stub receiver, and analytics event tracking."""
|
||||
UNENROLL_DONE.connect(self.assert_unenroll_signal)
|
||||
patcher = patch('student.models.tracker')
|
||||
self.mock_tracker = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def tearDown(self):
|
||||
"""Disconnects the UNENROLL stub receiver."""
|
||||
UNENROLL_DONE.disconnect(self.assert_unenroll_signal)
|
||||
|
||||
def assert_unenroll_signal(self, skip_refund=False, **kwargs): # pylint: disable=W0613
|
||||
""" Signal Receiver stub for testing that the unenroll signal was fired. """
|
||||
self.assertFalse(self.signal_fired)
|
||||
self.assertTrue(skip_refund)
|
||||
self.signal_fired = True
|
||||
|
||||
def test_transfer_students(self):
|
||||
student = UserFactory()
|
||||
""" Verify the transfer student command works as intended. """
|
||||
student = UserFactory.create()
|
||||
student.set_password(self.PASSWORD) # pylint: disable=E1101
|
||||
student.save() # pylint: disable=E1101
|
||||
|
||||
mode = 'verified'
|
||||
# Original Course
|
||||
original_course_location = locator.CourseLocator('Org0', 'Course0', 'Run0')
|
||||
course = self._create_course(original_course_location)
|
||||
# Enroll the student in 'verified'
|
||||
CourseEnrollment.enroll(student, course.id, mode="verified")
|
||||
|
||||
# Create and purchase a verified cert for the original course.
|
||||
self._create_and_purchase_verified(student, course.id)
|
||||
|
||||
# New Course 1
|
||||
course_location_one = locator.CourseLocator('Org1', 'Course1', 'Run1')
|
||||
new_course_one = self._create_course(course_location_one)
|
||||
@@ -45,11 +72,55 @@ class TestTransferStudents(ModuleStoreTestCase):
|
||||
transfer_students.Command().handle(
|
||||
source_course=original_key, dest_course_list=new_key_one + "," + new_key_two
|
||||
)
|
||||
self.assertTrue(self.signal_fired)
|
||||
|
||||
# Confirm the analytics event was emitted.
|
||||
self.mock_tracker.emit.assert_has_calls( # pylint: disable=E1103
|
||||
[
|
||||
call(
|
||||
EVENT_NAME_ENROLLMENT_ACTIVATED,
|
||||
{'course_id': original_key, 'user_id': student.id, 'mode': mode}
|
||||
),
|
||||
call(
|
||||
EVENT_NAME_ENROLLMENT_MODE_CHANGED,
|
||||
{'course_id': original_key, 'user_id': student.id, 'mode': mode}
|
||||
),
|
||||
call(
|
||||
EVENT_NAME_ENROLLMENT_DEACTIVATED,
|
||||
{'course_id': original_key, 'user_id': student.id, 'mode': mode}
|
||||
),
|
||||
call(
|
||||
EVENT_NAME_ENROLLMENT_ACTIVATED,
|
||||
{'course_id': new_key_one, 'user_id': student.id, 'mode': mode}
|
||||
),
|
||||
call(
|
||||
EVENT_NAME_ENROLLMENT_MODE_CHANGED,
|
||||
{'course_id': new_key_one, 'user_id': student.id, 'mode': mode}
|
||||
),
|
||||
call(
|
||||
EVENT_NAME_ENROLLMENT_ACTIVATED,
|
||||
{'course_id': new_key_two, 'user_id': student.id, 'mode': mode}
|
||||
),
|
||||
call(
|
||||
EVENT_NAME_ENROLLMENT_MODE_CHANGED,
|
||||
{'course_id': new_key_two, 'user_id': student.id, 'mode': mode}
|
||||
)
|
||||
]
|
||||
)
|
||||
self.mock_tracker.reset_mock()
|
||||
|
||||
# Confirm the enrollment mode is verified on the new courses, and enrollment is enabled as appropriate.
|
||||
self.assertEquals(('verified', False), CourseEnrollment.enrollment_mode_for_user(student, course.id))
|
||||
self.assertEquals(('verified', True), CourseEnrollment.enrollment_mode_for_user(student, new_course_one.id))
|
||||
self.assertEquals(('verified', True), CourseEnrollment.enrollment_mode_for_user(student, new_course_two.id))
|
||||
self.assertEquals((mode, False), CourseEnrollment.enrollment_mode_for_user(student, course.id))
|
||||
self.assertEquals((mode, True), CourseEnrollment.enrollment_mode_for_user(student, new_course_one.id))
|
||||
self.assertEquals((mode, True), CourseEnrollment.enrollment_mode_for_user(student, new_course_two.id))
|
||||
|
||||
# Confirm the student has not be refunded.
|
||||
target_certs = CertificateItem.objects.filter(
|
||||
course_id=course.id, user_id=student, status='purchased', mode=mode
|
||||
)
|
||||
self.assertTrue(target_certs[0])
|
||||
self.assertFalse(target_certs[0].refund_requested_time)
|
||||
self.assertEquals(target_certs[0].order.status, 'purchased')
|
||||
|
||||
def _create_course(self, course_location):
|
||||
""" Creates a course """
|
||||
@@ -58,3 +129,15 @@ class TestTransferStudents(ModuleStoreTestCase):
|
||||
number=course_location.course,
|
||||
run=course_location.run
|
||||
)
|
||||
|
||||
def _create_and_purchase_verified(self, student, course_id):
|
||||
""" Creates a verified mode for the course and purchases it for the student. """
|
||||
course_mode = CourseMode(course_id=course_id,
|
||||
mode_slug="verified",
|
||||
mode_display_name="verified cert",
|
||||
min_price=50)
|
||||
course_mode.save()
|
||||
# When there is no expiration date on a verified mode, the user can always get a refund
|
||||
cart = Order.get_cart_for_user(user=student)
|
||||
CertificateItem.add_to_order(cart, course_id, 50, 'verified')
|
||||
cart.purchase()
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from student.models import UserStanding
|
||||
|
||||
|
||||
class UserStandingMiddleware(object):
|
||||
"""
|
||||
Checks a user's standing on request. Returns a 403 if the user's
|
||||
|
||||
@@ -18,12 +18,10 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
db.send_create_signal('student', ['DashboardConfiguration'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'DashboardConfiguration'
|
||||
db.delete_table('student_dashboardconfiguration')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -176,4 +174,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -56,7 +56,7 @@ from ratelimitbackend import admin
|
||||
|
||||
import analytics
|
||||
|
||||
UNENROLL_DONE = Signal(providing_args=["course_enrollment"])
|
||||
UNENROLL_DONE = Signal(providing_args=["course_enrollment", "skip_refund"])
|
||||
log = logging.getLogger(__name__)
|
||||
AUDIT_LOG = logging.getLogger("audit")
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore # pylint: disable=invalid-name
|
||||
@@ -665,15 +665,19 @@ class LoginFailures(models.Model):
|
||||
class CourseEnrollmentException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NonExistentCourseError(CourseEnrollmentException):
|
||||
pass
|
||||
|
||||
|
||||
class EnrollmentClosedError(CourseEnrollmentException):
|
||||
pass
|
||||
|
||||
|
||||
class CourseFullError(CourseEnrollmentException):
|
||||
pass
|
||||
|
||||
|
||||
class AlreadyEnrolledError(CourseEnrollmentException):
|
||||
pass
|
||||
|
||||
@@ -776,7 +780,7 @@ class CourseEnrollment(models.Model):
|
||||
is_course_full = cls.num_enrolled_in(course.id) >= course.max_student_enrollments_allowed
|
||||
return is_course_full
|
||||
|
||||
def update_enrollment(self, mode=None, is_active=None, emit_unenrollment_event=True):
|
||||
def update_enrollment(self, mode=None, is_active=None, skip_refund=False):
|
||||
"""
|
||||
Updates an enrollment for a user in a class. This includes options
|
||||
like changing the mode, toggling is_active True/False, etc.
|
||||
@@ -814,8 +818,8 @@ class CourseEnrollment(models.Model):
|
||||
u"mode:{}".format(self.mode)]
|
||||
)
|
||||
|
||||
elif emit_unenrollment_event:
|
||||
UNENROLL_DONE.send(sender=None, course_enrollment=self)
|
||||
else:
|
||||
UNENROLL_DONE.send(sender=None, course_enrollment=self, skip_refund=skip_refund)
|
||||
|
||||
self.emit_event(EVENT_NAME_ENROLLMENT_DEACTIVATED)
|
||||
|
||||
@@ -988,7 +992,7 @@ class CourseEnrollment(models.Model):
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def unenroll(cls, user, course_id, emit_unenrollment_event=True):
|
||||
def unenroll(cls, user, course_id, skip_refund=False):
|
||||
"""
|
||||
Remove the user from a given course. If the relevant `CourseEnrollment`
|
||||
object doesn't exist, we log an error but don't throw an exception.
|
||||
@@ -999,11 +1003,11 @@ class CourseEnrollment(models.Model):
|
||||
|
||||
`course_id` is our usual course_id string (e.g. "edX/Test101/2013_Fall)
|
||||
|
||||
`emit_unenrollment_events` can be set to False to suppress events firing.
|
||||
`skip_refund` can be set to True to avoid the refund process.
|
||||
"""
|
||||
try:
|
||||
record = CourseEnrollment.objects.get(user=user, course_id=course_id)
|
||||
record.update_enrollment(is_active=False, emit_unenrollment_event=emit_unenrollment_event)
|
||||
record.update_enrollment(is_active=False, skip_refund=skip_refund)
|
||||
|
||||
except cls.DoesNotExist:
|
||||
err_msg = u"Tried to unenroll student {} from {} but they were not enrolled"
|
||||
|
||||
@@ -210,6 +210,7 @@ class CourseFinanceAdminRole(CourseRole):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CourseFinanceAdminRole, self).__init__(self.ROLE, *args, **kwargs)
|
||||
|
||||
|
||||
class CourseBetaTesterRole(CourseRole):
|
||||
"""A course Beta Tester"""
|
||||
ROLE = 'beta_testers'
|
||||
|
||||
@@ -11,6 +11,7 @@ from opaque_keys.edx.locator import CourseLocator
|
||||
from mock import patch
|
||||
import ddt
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
|
||||
"""
|
||||
|
||||
@@ -105,7 +105,10 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
course_location = SlashSeparatedCourseKey('testOrg', 'erroredCourse', 'RunBabyRun')
|
||||
course = self._create_course_with_access_groups(course_location)
|
||||
course_db_record = mongo_store._find_one(course.location)
|
||||
course_db_record.setdefault('metadata', {}).get('tabs', []).append({"type": "wiko", "name": "Wiki" })
|
||||
course_db_record.setdefault('metadata', {}).get('tabs', []).append({
|
||||
"type": "wiko",
|
||||
"name": "Wiki",
|
||||
})
|
||||
mongo_store.collection.update(
|
||||
{'_id': course.location.to_deprecated_son()},
|
||||
{'$set': {
|
||||
|
||||
@@ -485,6 +485,7 @@ class LoginOAuthTokenMixin(object):
|
||||
self._setup_user_response(success=True)
|
||||
response = self.client.post(self.url, {"access_token": "dummy"})
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertEqual(self.client.session['_auth_user_id'], self.user.id)
|
||||
|
||||
def test_invalid_token(self):
|
||||
self._setup_user_response(success=False)
|
||||
|
||||
@@ -25,6 +25,7 @@ FAKE_MICROSITE = {
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def fake_site_name(name, default=None): # pylint: disable=W0613
|
||||
"""
|
||||
create a fake microsite site name
|
||||
@@ -34,12 +35,14 @@ def fake_site_name(name, default=None): # pylint: disable=W0613
|
||||
else:
|
||||
return default
|
||||
|
||||
|
||||
def fake_microsite_get_value(name, default=None): # pylint: disable=W0613
|
||||
"""
|
||||
create a fake microsite site name
|
||||
"""
|
||||
return FAKE_MICROSITE.get(name, default)
|
||||
|
||||
|
||||
class TestMicrosite(TestCase):
|
||||
"""Test for Account Creation from a white labeled Micro-Sites"""
|
||||
def setUp(self):
|
||||
|
||||
@@ -15,6 +15,7 @@ from edxmako.tests import mako_middleware_process_request
|
||||
from external_auth.models import ExternalAuthMap
|
||||
from student.views import create_account
|
||||
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {'ENFORCE_PASSWORD_POLICY': True})
|
||||
class TestPasswordPolicy(TestCase):
|
||||
"""
|
||||
|
||||
@@ -265,6 +265,7 @@ class DashboardTest(ModuleStoreTestCase):
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@patch('courseware.views.log.warning')
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
|
||||
def test_blocked_course_scenario(self, log_warning):
|
||||
|
||||
self.client.login(username="jack", password="test")
|
||||
|
||||
@@ -1115,6 +1115,7 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un
|
||||
}) # TODO: this should be status code 400 # pylint: disable=fixme
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
@social_utils.strategy("social:complete")
|
||||
def login_oauth_token(request, backend):
|
||||
@@ -1135,6 +1136,7 @@ def login_oauth_token(request, backend):
|
||||
pass
|
||||
# do_auth can return a non-User object if it fails
|
||||
if user and isinstance(user, User):
|
||||
login(request, user)
|
||||
return JsonResponse(status=204)
|
||||
else:
|
||||
# Ensure user does not re-enter the pipeline
|
||||
@@ -1791,11 +1793,9 @@ def activate_account(request, key):
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def password_reset(request):
|
||||
""" Attempts to send a password reset e-mail. """
|
||||
if request.method != "POST":
|
||||
raise Http404
|
||||
|
||||
# Add some rate limiting here by re-using the RateLimitMixin as a helper class
|
||||
limiter = BadRequestRateLimiter()
|
||||
if limiter.is_rate_limit_exceeded(request):
|
||||
|
||||
@@ -95,7 +95,9 @@ def initial_setup(server):
|
||||
|
||||
if browser_driver == 'chrome':
|
||||
desired_capabilities = DesiredCapabilities.CHROME
|
||||
desired_capabilities['loggingPrefs'] = { 'browser':'ALL' }
|
||||
desired_capabilities['loggingPrefs'] = {
|
||||
'browser': 'ALL',
|
||||
}
|
||||
elif browser_driver == 'firefox':
|
||||
desired_capabilities = DesiredCapabilities.FIREFOX
|
||||
else:
|
||||
@@ -239,7 +241,7 @@ def capture_console_log(scenario):
|
||||
output_dir = '{}/log'.format(settings.TEST_ROOT)
|
||||
file_name = '{}/{}.log'.format(output_dir, scenario.name.replace(' ', '_'))
|
||||
|
||||
with open (file_name, 'w') as output_file:
|
||||
with open(file_name, 'w') as output_file:
|
||||
for line in log:
|
||||
output_file.write("{}{}".format(dumps(line), '\n'))
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import re
|
||||
import urlparse
|
||||
from .http import StubHttpRequestHandler, StubHttpService
|
||||
|
||||
|
||||
class StubCommentsServiceHandler(StubHttpRequestHandler):
|
||||
|
||||
@property
|
||||
|
||||
@@ -123,8 +123,8 @@ class StubHttpRequestHandler(BaseHTTPRequestHandler, object):
|
||||
# By default, `parse_qs` returns a list of values for each param
|
||||
# For convenience, we replace lists of 1 element with just the element
|
||||
return {
|
||||
k:v[0] if len(v) == 1 else v
|
||||
for k,v in urlparse.parse_qs(query).items()
|
||||
key: value[0] if len(value) == 1 else value
|
||||
for key, value in urlparse.parse_qs(query).items()
|
||||
}
|
||||
|
||||
@lazy
|
||||
|
||||
@@ -21,6 +21,7 @@ import mock
|
||||
import requests
|
||||
from http import StubHttpRequestHandler, StubHttpService
|
||||
|
||||
|
||||
class StubLtiHandler(StubHttpRequestHandler):
|
||||
"""
|
||||
A handler for LTI POST and GET requests.
|
||||
|
||||
@@ -45,7 +45,7 @@ class StudentState(object):
|
||||
|
||||
@property
|
||||
def num_pending(self):
|
||||
return max(self.INITIAL_ESSAYS_AVAILABLE- self.num_graded, 0)
|
||||
return max(self.INITIAL_ESSAYS_AVAILABLE - self.num_graded, 0)
|
||||
|
||||
@property
|
||||
def num_required(self):
|
||||
@@ -300,7 +300,6 @@ class StubOraHandler(StubHttpRequestHandler):
|
||||
"""
|
||||
self._success_response({'problem_list': self.server.problem_list})
|
||||
|
||||
|
||||
@require_params('POST', 'grader_id', 'location', 'submission_id', 'score', 'feedback', 'submission_key')
|
||||
def _save_grade(self):
|
||||
"""
|
||||
@@ -421,7 +420,6 @@ class StubOraHandler(StubHttpRequestHandler):
|
||||
)
|
||||
self.send_response(400)
|
||||
|
||||
|
||||
def _student(self, method, key='student_id'):
|
||||
"""
|
||||
Return the `StudentState` instance for the student ID given
|
||||
|
||||
@@ -25,7 +25,9 @@ class StubHttpServiceTest(unittest.TestCase):
|
||||
'test_empty': '',
|
||||
'test_int': 12345,
|
||||
'test_float': 123.45,
|
||||
'test_dict': { 'test_key': 'test_val' },
|
||||
'test_dict': {
|
||||
'test_key': 'test_val',
|
||||
},
|
||||
'test_empty_dict': {},
|
||||
'test_unicode': u'\u2603 the snowman',
|
||||
'test_none': None,
|
||||
|
||||
@@ -7,6 +7,7 @@ import urllib2
|
||||
import requests
|
||||
from terrain.stubs.lti import StubLtiService
|
||||
|
||||
|
||||
class StubLtiServiceTest(unittest.TestCase):
|
||||
"""
|
||||
A stub of the LTI provider that listens on a local
|
||||
@@ -34,7 +35,7 @@ class StubLtiServiceTest(unittest.TestCase):
|
||||
'launch_presentation_return_url': '',
|
||||
'lis_outcome_service_url': 'http://localhost:8001/test_callback',
|
||||
'lis_result_sourcedid': '',
|
||||
'resource_link_id':'',
|
||||
'resource_link_id': '',
|
||||
}
|
||||
|
||||
def test_invalid_request_url(self):
|
||||
|
||||
@@ -9,6 +9,7 @@ import os
|
||||
from logging import getLogger
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class VideoSourceRequestHandler(SimpleHTTPRequestHandler):
|
||||
"""
|
||||
Request handler for serving video sources locally.
|
||||
|
||||
@@ -214,6 +214,7 @@ class StubXQueueService(StubHttpService):
|
||||
except for 'default' and 'register_submission_url' which have special meaning
|
||||
"""
|
||||
return {
|
||||
key:val for key, val in self.config.iteritems()
|
||||
key: value
|
||||
for key, value in self.config.iteritems()
|
||||
if key not in self.NON_QUEUE_CONFIG_KEYS
|
||||
}.items()
|
||||
|
||||
@@ -106,7 +106,7 @@ class TrackMiddleware(object):
|
||||
for header_name, context_key in META_KEY_TO_CONTEXT_KEY.iteritems():
|
||||
context[context_key] = request.META.get(header_name, '')
|
||||
|
||||
# Google Analytics uses the clientId to keep track of unique visitors. A GA cookie looks like
|
||||
# Google Analytics uses the clientId to keep track of unique visitors. A GA cookie looks like
|
||||
# this: _ga=GA1.2.1033501218.1368477899. The clientId is this part: 1033501218.1368477899.
|
||||
google_analytics_cookie = request.COOKIES.get('_ga')
|
||||
if google_analytics_cookie is None:
|
||||
|
||||
@@ -5,6 +5,7 @@ from student.tests.factories import UserFactory
|
||||
from user_api.models import UserPreference, UserCourseTag
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
# Factories don't have __init__ methods, and are self documenting
|
||||
# pylint: disable=W0232, C0111
|
||||
class UserPreferenceFactory(DjangoModelFactory):
|
||||
|
||||
@@ -298,7 +298,7 @@ class AccountApiTest(TestCase):
|
||||
if create_inactive_account:
|
||||
# Create an account, but do not activate it
|
||||
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
||||
|
||||
|
||||
account_api.request_password_change(self.EMAIL, self.ORIG_HOST, self.IS_SECURE)
|
||||
|
||||
# Verify that no email messages have been sent
|
||||
|
||||
@@ -4,6 +4,7 @@ which can be used for rate limiting
|
||||
"""
|
||||
from ratelimitbackend.backends import RateLimitMixin
|
||||
|
||||
|
||||
class BadRequestRateLimiter(RateLimitMixin):
|
||||
"""
|
||||
Use the 3rd party RateLimitMixin to help do rate limiting on the Password Reset flows
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user