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:
Will Daly
2014-11-13 11:16:16 -05:00
257 changed files with 1195 additions and 440 deletions

View File

@@ -1 +0,0 @@

View File

@@ -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')

View File

@@ -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

View 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'''

View File

@@ -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))

View File

@@ -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.

View File

@@ -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":

View File

@@ -10,4 +10,3 @@ class Command(BaseCommand):
raise CommandError("restore_asset_from_trashcan requires one argument: <location>")
restore_asset_from_trashcan(args[0])

View File

@@ -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

View File

@@ -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": [

View File

@@ -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. """

View File

@@ -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.

View File

@@ -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.

View File

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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View 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();
}
};
});

View File

@@ -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) {

View 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);
});
});
});
}
);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}">

View File

@@ -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>

View File

@@ -455,7 +455,7 @@
</article>
</section>
</article>
<aside class="content-supplementary" role="complimentary">
<aside class="content-supplementary" role="complementary">
<!-- begin publishing changes 1 -->

View File

@@ -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">

View File

@@ -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>

View File

@@ -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()

View File

@@ -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

View File

@@ -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.

View File

@@ -209,6 +209,7 @@ class ListCohortsTestCase(CohortViewsTestCase):
actual_cohorts,
)
class AddCohortTestCase(CohortViewsTestCase):
"""
Tests the `add_cohort` view.

View File

@@ -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 = {

View File

@@ -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"] = [

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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.

View File

@@ -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

View File

@@ -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):
"""

View File

@@ -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')

View File

@@ -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):
"""

View File

@@ -8,6 +8,7 @@ from django.db.utils import DatabaseError
import mock
from django.test.testcases import TestCase
class HeartbeatTestCase(TestCase):
"""
Test the heartbeat

View File

@@ -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 %}

View File

@@ -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):
"""

View File

@@ -1,3 +1,3 @@
# Register signal handlers
import signals
import exceptions
import exceptions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -65,4 +65,3 @@ class Command(BaseCommand):
))
except IOError:
raise CommandError("Error writing to file: %s" % output_filename)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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']

View File

@@ -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"

View File

@@ -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'

View File

@@ -11,6 +11,7 @@ from opaque_keys.edx.locator import CourseLocator
from mock import patch
import ddt
@ddt.ddt
class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
"""

View File

@@ -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': {

View File

@@ -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)

View File

@@ -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):

View File

@@ -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):
"""

View File

@@ -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")

View File

@@ -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):

View File

@@ -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'))

View File

@@ -6,6 +6,7 @@ import re
import urlparse
from .http import StubHttpRequestHandler, StubHttpService
class StubCommentsServiceHandler(StubHttpRequestHandler):
@property

View File

@@ -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

View File

@@ -21,6 +21,7 @@ import mock
import requests
from http import StubHttpRequestHandler, StubHttpService
class StubLtiHandler(StubHttpRequestHandler):
"""
A handler for LTI POST and GET requests.

View File

@@ -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

View File

@@ -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,

View File

@@ -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):

View File

@@ -9,6 +9,7 @@ import os
from logging import getLogger
LOGGER = getLogger(__name__)
class VideoSourceRequestHandler(SimpleHTTPRequestHandler):
"""
Request handler for serving video sources locally.

View File

@@ -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()

View File

@@ -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:

View File

@@ -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):

View File

@@ -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

View File

@@ -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