Merge branch 'master' of https://github.com/edx/edx-platform
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
"""
|
||||
Configuration for Studio API Django application
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
"""
|
||||
URLs for the Studio API app
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.conf.urls import include, url
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
"""
|
||||
URLs for the Studio API [Course Run]
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from .views.course_runs import CourseRunViewSet
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
"""HTTP endpoints for the Course Run API."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import Http404
|
||||
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
||||
@@ -8,6 +12,7 @@ from rest_framework.decorators import detail_route
|
||||
from rest_framework.response import Response
|
||||
|
||||
from contentstore.views.course import _accessible_courses_iter, get_course_and_check_access
|
||||
|
||||
from ..serializers.course_runs import (
|
||||
CourseRunCreateSerializer,
|
||||
CourseRunImageSerializer,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Utilities for returning XModule JS (used by requirejs)
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from django.conf import settings
|
||||
from mako.exceptions import TopLevelLookupException
|
||||
from mako.lookup import TemplateLookup
|
||||
|
||||
from openedx.core.djangoapps.theming.helpers import get_template as themed_template
|
||||
from openedx.core.djangoapps.theming.helpers import get_template_path_with_theme, strip_site_theme_templates_path
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
|
||||
@@ -63,7 +62,7 @@ class DynamicTemplateLookup(TemplateLookup):
|
||||
self._collection.clear()
|
||||
self._uri_cache.clear()
|
||||
|
||||
def adjust_uri(self, uri, calling_uri):
|
||||
def adjust_uri(self, uri, relativeto):
|
||||
"""
|
||||
This method is called by mako when including a template in another template or when inheriting an existing mako
|
||||
template. The method adjusts the `uri` to make it relative to the calling template's location.
|
||||
@@ -76,12 +75,12 @@ class DynamicTemplateLookup(TemplateLookup):
|
||||
that template lookup skips the current theme and looks up the built-in template in standard locations.
|
||||
"""
|
||||
# Make requested uri relative to the calling uri.
|
||||
relative_uri = super(DynamicTemplateLookup, self).adjust_uri(uri, calling_uri)
|
||||
# Is the calling template (calling_uri) which is including or inheriting current template (uri)
|
||||
relative_uri = super(DynamicTemplateLookup, self).adjust_uri(uri, relativeto)
|
||||
# Is the calling template (relativeto) which is including or inheriting current template (uri)
|
||||
# located inside a theme?
|
||||
if calling_uri != strip_site_theme_templates_path(calling_uri):
|
||||
if relativeto != strip_site_theme_templates_path(relativeto):
|
||||
# Is the calling template trying to include/inherit itself?
|
||||
if calling_uri == get_template_path_with_theme(relative_uri):
|
||||
if relativeto == get_template_path_with_theme(relative_uri):
|
||||
return TopLevelTemplateURI(relative_uri)
|
||||
return relative_uri
|
||||
|
||||
@@ -96,20 +95,14 @@ class DynamicTemplateLookup(TemplateLookup):
|
||||
If still unable to find a template, it will fallback to the default template directories after stripping off
|
||||
the prefix path to theme.
|
||||
"""
|
||||
# try to get template for the given file from microsite
|
||||
template = themed_template(uri)
|
||||
|
||||
# if microsite template is not present or request is not in microsite then
|
||||
# let mako find and serve a template
|
||||
if not template:
|
||||
if isinstance(uri, TopLevelTemplateURI):
|
||||
if isinstance(uri, TopLevelTemplateURI):
|
||||
template = self._get_toplevel_template(uri)
|
||||
else:
|
||||
try:
|
||||
# Try to find themed template, i.e. see if current theme overrides the template
|
||||
template = super(DynamicTemplateLookup, self).get_template(get_template_path_with_theme(uri))
|
||||
except TopLevelLookupException:
|
||||
template = self._get_toplevel_template(uri)
|
||||
else:
|
||||
try:
|
||||
# Try to find themed template, i.e. see if current theme overrides the template
|
||||
template = super(DynamicTemplateLookup, self).get_template(get_template_path_with_theme(uri))
|
||||
except TopLevelLookupException:
|
||||
template = self._get_toplevel_template(uri)
|
||||
|
||||
return template
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ from student.models import (
|
||||
from student.signals import REFUND_ORDER
|
||||
from student.tasks import send_activation_email
|
||||
from student.text_me_the_app import TextMeTheAppFragmentView
|
||||
from util.bad_request_rate_limiter import BadRequestRateLimiter
|
||||
from util.request_rate_limiter import BadRequestRateLimiter, PasswordResetEmailRateLimiter
|
||||
from util.db import outer_atomic
|
||||
from util.json_request import JsonResponse
|
||||
from util.password_policy_validators import normalize_password, validate_password
|
||||
@@ -664,10 +664,14 @@ def password_change_request_handler(request):
|
||||
|
||||
"""
|
||||
|
||||
limiter = BadRequestRateLimiter()
|
||||
if limiter.is_rate_limit_exceeded(request):
|
||||
password_reset_email_limiter = PasswordResetEmailRateLimiter()
|
||||
|
||||
if password_reset_email_limiter.is_rate_limit_exceeded(request):
|
||||
AUDIT_LOG.warning("Password reset rate limit exceeded")
|
||||
return HttpResponseForbidden()
|
||||
return HttpResponse(
|
||||
_("Your previous request is in progress, please try again in a few moments."),
|
||||
status=403
|
||||
)
|
||||
|
||||
user = request.user
|
||||
# Prefer logged-in user's email
|
||||
@@ -681,9 +685,6 @@ def password_change_request_handler(request):
|
||||
destroy_oauth_tokens(user)
|
||||
except UserNotFound:
|
||||
AUDIT_LOG.info("Invalid password reset attempt")
|
||||
# Increment the rate limit counter
|
||||
limiter.tick_bad_request_counter(request)
|
||||
|
||||
# If enabled, send an email saying that a password reset was attempted, but that there is
|
||||
# no user associated with the email
|
||||
if configuration_helpers.get_value('ENABLE_PASSWORD_RESET_FAILURE_EMAIL',
|
||||
@@ -703,13 +704,13 @@ def password_change_request_handler(request):
|
||||
language=settings.LANGUAGE_CODE,
|
||||
user_context=message_context,
|
||||
)
|
||||
|
||||
ace.send(msg)
|
||||
except UserAPIInternalError as err:
|
||||
log.exception('Error occured during password change for user {email}: {error}'
|
||||
.format(email=email, error=err))
|
||||
return HttpResponse(_("Some error occured during password change. Please try again"), status=500)
|
||||
|
||||
password_reset_email_limiter.tick_request_counter(request)
|
||||
return HttpResponse(status=200)
|
||||
else:
|
||||
return HttpResponseBadRequest(_("No email address provided."))
|
||||
@@ -770,7 +771,7 @@ def password_reset(request):
|
||||
else:
|
||||
# bad user? tick the rate limiter counter
|
||||
AUDIT_LOG.info("Bad password_reset user passed in.")
|
||||
limiter.tick_bad_request_counter(request)
|
||||
limiter.tick_request_counter(request)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
"""
|
||||
A utility class which wraps the RateLimitMixin 3rd party class to do bad request counting
|
||||
which can be used for rate limiting
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from ratelimitbackend.backends import RateLimitMixin
|
||||
|
||||
|
||||
class BadRequestRateLimiter(RateLimitMixin):
|
||||
"""
|
||||
Use the 3rd party RateLimitMixin to help do rate limiting on the Password Reset flows
|
||||
"""
|
||||
|
||||
def is_rate_limit_exceeded(self, request):
|
||||
"""
|
||||
Returns if the client has been rated limited
|
||||
"""
|
||||
counts = self.get_counters(request)
|
||||
return sum(counts.values()) >= self.requests
|
||||
|
||||
def tick_bad_request_counter(self, request):
|
||||
"""
|
||||
Ticks any counters used to compute when rate limt has been reached
|
||||
"""
|
||||
self.cache_incr(self.get_cache_key(request))
|
||||
59
common/djangoapps/util/request_rate_limiter.py
Normal file
59
common/djangoapps/util/request_rate_limiter.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
A utility class which wraps the RateLimitMixin 3rd party class to do bad request counting
|
||||
which can be used for rate limiting
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.conf import settings
|
||||
from ratelimitbackend.backends import RateLimitMixin
|
||||
|
||||
|
||||
class RequestRateLimiter(RateLimitMixin):
|
||||
"""
|
||||
Use the 3rd party RateLimitMixin to help do rate limiting.
|
||||
"""
|
||||
def is_rate_limit_exceeded(self, request):
|
||||
"""
|
||||
Returns if the client has been rated limited
|
||||
"""
|
||||
counts = self.get_counters(request)
|
||||
return sum(counts.values()) >= self.requests
|
||||
|
||||
def tick_request_counter(self, request):
|
||||
"""
|
||||
Ticks any counters used to compute when rate limt has been reached
|
||||
"""
|
||||
self.cache_incr(self.get_cache_key(request))
|
||||
|
||||
|
||||
class BadRequestRateLimiter(RequestRateLimiter):
|
||||
"""
|
||||
Default rate limit is 30 requests for every 5 minutes.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PasswordResetEmailRateLimiter(RequestRateLimiter):
|
||||
"""
|
||||
Rate limiting requests to send password reset emails.
|
||||
"""
|
||||
email_rate_limit = getattr(settings, 'PASSWORD_RESET_EMAIL_RATE_LIMIT', {})
|
||||
requests = email_rate_limit.get('no_of_emails', 1)
|
||||
cache_timeout_seconds = email_rate_limit.get('per_seconds', 60)
|
||||
reset_email_cache_prefix = 'resetemail'
|
||||
|
||||
def key(self, request, dt):
|
||||
"""
|
||||
Returns cache key.
|
||||
"""
|
||||
return '%s-%s-%s' % (
|
||||
self.reset_email_cache_prefix,
|
||||
self.get_ip(request),
|
||||
dt.strftime('%Y%m%d%H%M'),
|
||||
)
|
||||
|
||||
def expire_after(self):
|
||||
"""
|
||||
Returns timeout for cache keys.
|
||||
"""
|
||||
return self.cache_timeout_seconds
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
036aaf0d4bf285266fc31c536191f5afc5b81d60
|
||||
d808f200f0f77beb2f243e204446a48bdb056ee1
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -366,7 +366,7 @@ CREATE TABLE `auth_permission` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `auth_permission_content_type_id_codename_01ab375a_uniq` (`content_type_id`,`codename`),
|
||||
CONSTRAINT `auth_permission_content_type_id_2f476e4b_fk_django_co` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2318 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2321 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `auth_registration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -2277,7 +2277,7 @@ CREATE TABLE `django_content_type` (
|
||||
`model` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `django_content_type_app_label_model_76bd3d3b_uniq` (`app_label`,`model`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=770 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=771 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `django_migrations`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -2288,7 +2288,7 @@ CREATE TABLE `django_migrations` (
|
||||
`name` varchar(255) NOT NULL,
|
||||
`applied` datetime(6) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=554 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=555 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `django_openid_auth_association`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -3351,6 +3351,32 @@ CREATE TABLE `grades_coursepersistentgradesflag` (
|
||||
CONSTRAINT `grades_coursepersist_changed_by_id_c8c392d6_fk_auth_user` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `grades_historicalpersistentsubsectiongradeoverride`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `grades_historicalpersistentsubsectiongradeoverride` (
|
||||
`id` int(11) NOT NULL,
|
||||
`created` datetime(6) NOT NULL,
|
||||
`modified` datetime(6) NOT NULL,
|
||||
`earned_all_override` double DEFAULT NULL,
|
||||
`possible_all_override` double DEFAULT NULL,
|
||||
`earned_graded_override` double DEFAULT NULL,
|
||||
`possible_graded_override` double DEFAULT NULL,
|
||||
`history_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`history_date` datetime(6) NOT NULL,
|
||||
`history_change_reason` varchar(100) DEFAULT NULL,
|
||||
`history_type` varchar(1) NOT NULL,
|
||||
`grade_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`history_user_id` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`history_id`),
|
||||
KEY `grades_historicalper_history_user_id_05000562_fk_auth_user` (`history_user_id`),
|
||||
KEY `grades_historicalpersistentsubsectiongradeoverride_id_e30d8953` (`id`),
|
||||
KEY `grades_historicalpersistent_created_e5fb4d96` (`created`),
|
||||
KEY `grades_historicalpersistent_modified_7355e846` (`modified`),
|
||||
KEY `grades_historicalpersistent_grade_id_ecfb45cc` (`grade_id`),
|
||||
CONSTRAINT `grades_historicalper_history_user_id_05000562_fk_auth_user` FOREIGN KEY (`history_user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `grades_persistentcoursegrade`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
|
||||
@@ -36,7 +36,7 @@ CREATE TABLE `django_migrations` (
|
||||
`name` varchar(255) NOT NULL,
|
||||
`applied` datetime(6) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=554 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=555 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from lms.djangoapps.certificates.models import (
|
||||
GeneratedCertificate,
|
||||
certificate_status_for_student
|
||||
)
|
||||
from util.bad_request_rate_limiter import BadRequestRateLimiter
|
||||
from util.request_rate_limiter import BadRequestRateLimiter
|
||||
from util.json_request import JsonResponse, JsonResponseBadRequest
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
@@ -171,12 +171,12 @@ def update_example_certificate(request):
|
||||
|
||||
if 'xqueue_body' not in request.POST:
|
||||
log.info(u"Missing parameter 'xqueue_body' for update example certificate end-point")
|
||||
rate_limiter.tick_bad_request_counter(request)
|
||||
rate_limiter.tick_request_counter(request)
|
||||
return JsonResponseBadRequest("Parameter 'xqueue_body' is required.")
|
||||
|
||||
if 'xqueue_header' not in request.POST:
|
||||
log.info(u"Missing parameter 'xqueue_header' for update example certificate end-point")
|
||||
rate_limiter.tick_bad_request_counter(request)
|
||||
rate_limiter.tick_request_counter(request)
|
||||
return JsonResponseBadRequest("Parameter 'xqueue_header' is required.")
|
||||
|
||||
try:
|
||||
@@ -184,7 +184,7 @@ def update_example_certificate(request):
|
||||
xqueue_header = json.loads(request.POST['xqueue_header'])
|
||||
except (ValueError, TypeError):
|
||||
log.info(u"Could not decode params to example certificate end-point as JSON.")
|
||||
rate_limiter.tick_bad_request_counter(request)
|
||||
rate_limiter.tick_request_counter(request)
|
||||
return JsonResponseBadRequest("Parameters must be JSON-serialized.")
|
||||
|
||||
# Attempt to retrieve the example certificate record
|
||||
@@ -199,7 +199,7 @@ def update_example_certificate(request):
|
||||
# from the XQueue. Return a 404 and increase the bad request counter
|
||||
# to protect against a DDOS attack.
|
||||
log.info(u"Could not find example certificate with uuid '%s' and access key '%s'", uuid, access_key)
|
||||
rate_limiter.tick_bad_request_counter(request)
|
||||
rate_limiter.tick_request_counter(request)
|
||||
raise Http404
|
||||
|
||||
if 'error' in xqueue_body:
|
||||
@@ -217,7 +217,7 @@ def update_example_certificate(request):
|
||||
# so we can display the example certificate.
|
||||
download_url = xqueue_body.get('url')
|
||||
if download_url is None:
|
||||
rate_limiter.tick_bad_request_counter(request)
|
||||
rate_limiter.tick_request_counter(request)
|
||||
log.warning(u"No download URL provided for example certificate with uuid '%s'.", uuid)
|
||||
return JsonResponseBadRequest(
|
||||
"Parameter 'download_url' is required for successfully generated certificates."
|
||||
|
||||
@@ -253,7 +253,7 @@ class AboutTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase, EventTra
|
||||
if course_visibility == COURSE_VISIBILITY_PUBLIC or course_visibility == COURSE_VISIBILITY_PUBLIC_OUTLINE:
|
||||
self.assertIn("View Course", resp.content)
|
||||
else:
|
||||
self.assertIn("Enroll in", resp.content)
|
||||
self.assertIn("Enroll Now", resp.content)
|
||||
|
||||
|
||||
class AboutTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
@@ -388,7 +388,7 @@ class AboutWithInvitationOnly(SharedModuleStoreTestCase):
|
||||
url = reverse('about_course', args=[text_type(self.course.id)])
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertIn(u"Enroll in {}".format(self.course.id.course), resp.content.decode('utf-8'))
|
||||
self.assertIn(u"Enroll Now", resp.content.decode('utf-8'))
|
||||
|
||||
# Check that registration button is present
|
||||
self.assertIn(REG_STR, resp.content)
|
||||
|
||||
@@ -316,7 +316,7 @@ class TestSites(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
url = reverse('about_course', args=[text_type(self.course_with_visibility.id)])
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertIn(u"Enroll in {}".format(self.course_with_visibility.id.course), resp.content.decode(resp.charset))
|
||||
self.assertIn(u"Enroll Now", resp.content.decode(resp.charset))
|
||||
self.assertNotIn(u"Add {} to Cart ($10)".format(
|
||||
self.course_with_visibility.id.course),
|
||||
resp.content.decode(resp.charset)
|
||||
@@ -326,10 +326,7 @@ class TestSites(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
url = reverse('about_course', args=[text_type(self.course_with_visibility.id)])
|
||||
resp = self.client.get(url, HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertNotIn(u"Enroll in {}".format(
|
||||
self.course_with_visibility.id.course),
|
||||
resp.content.decode(resp.charset)
|
||||
)
|
||||
self.assertNotIn(u"Enroll Now", resp.content.decode(resp.charset))
|
||||
self.assertIn(u"Add {} to Cart <span>($10 USD)</span>".format(
|
||||
self.course_with_visibility.id.course
|
||||
), resp.content.decode(resp.charset))
|
||||
|
||||
@@ -65,40 +65,6 @@ DASHBOARD_INFO_FLAG = WaffleFlag(experiments_namespace,
|
||||
flag_undefined_default=True)
|
||||
# TODO END: clean up as part of REVEM-199 (End)
|
||||
|
||||
# .. toggle_name: experiments.add_audit_deadline
|
||||
# .. toggle_type: feature_flag
|
||||
# .. toggle_default: True
|
||||
# .. toggle_description: Toggle for adding the current course's audit deadline
|
||||
# .. toggle_category: experiments
|
||||
# .. toggle_use_cases: monitored_rollout
|
||||
# .. toggle_creation_date: 2019-5-7
|
||||
# .. toggle_expiration_date: None
|
||||
# .. toggle_warnings: None
|
||||
# .. toggle_tickets: REVEM-329
|
||||
# .. toggle_status: supported
|
||||
AUDIT_DEADLINE_FLAG = WaffleFlag(
|
||||
waffle_namespace=experiments_namespace,
|
||||
flag_name=u'add_audit_deadline',
|
||||
flag_undefined_default=True
|
||||
)
|
||||
|
||||
# .. toggle_name: experiments.deprecated_metadata
|
||||
# .. toggle_type: feature_flag
|
||||
# .. toggle_default: True
|
||||
# .. toggle_description: Toggle for using the deprecated method for calculating user metadata
|
||||
# .. toggle_category: experiments
|
||||
# .. toggle_use_cases: monitored_rollout
|
||||
# .. toggle_creation_date: 2019-5-8
|
||||
# .. toggle_expiration_date: None
|
||||
# .. toggle_warnings: None
|
||||
# .. toggle_tickets: REVEM-350
|
||||
# .. toggle_status: supported
|
||||
DEPRECATED_METADATA = WaffleFlag(
|
||||
waffle_namespace=experiments_namespace,
|
||||
flag_name=u'deprecated_metadata',
|
||||
flag_undefined_default=False
|
||||
)
|
||||
|
||||
|
||||
def check_and_get_upgrade_link_and_date(user, enrollment=None, course=None):
|
||||
"""
|
||||
@@ -288,9 +254,6 @@ def get_experiment_user_metadata_context(course, user):
|
||||
"""
|
||||
Return a context dictionary with the keys used by the user_metadata.html.
|
||||
"""
|
||||
if DEPRECATED_METADATA.is_enabled():
|
||||
return get_deprecated_experiment_user_metadata_context(course, user)
|
||||
|
||||
enrollment = None
|
||||
# TODO: clean up as part of REVO-28 (START)
|
||||
user_enrollments = None
|
||||
@@ -329,122 +292,6 @@ def get_experiment_user_metadata_context(course, user):
|
||||
return context
|
||||
|
||||
|
||||
# pylint: disable=too-many-statements
|
||||
def get_deprecated_experiment_user_metadata_context(course, user):
|
||||
"""
|
||||
Return a context dictionary with the keys used by the user_metadata.html. This is deprecated and will be removed
|
||||
once we have confirmed that its replacement functions as intended.
|
||||
"""
|
||||
enrollment_mode = None
|
||||
enrollment_time = None
|
||||
enrollment = None
|
||||
# TODO: clean up as part of REVO-28 (START)
|
||||
has_non_audit_enrollments = None
|
||||
# TODO: clean up as part of REVO-28 (END)
|
||||
# TODO: clean up as part of REVEM-199 (START)
|
||||
program_key = None
|
||||
# TODO: clean up as part of REVEM-199 (END)
|
||||
try:
|
||||
# TODO: clean up as part of REVO-28 (START)
|
||||
user_enrollments = CourseEnrollment.objects.select_related('course').filter(user_id=user.id)
|
||||
audit_enrollments = user_enrollments.filter(mode='audit')
|
||||
has_non_audit_enrollments = (len(audit_enrollments) != len(user_enrollments))
|
||||
# TODO: clean up as part of REVO-28 (END)
|
||||
# TODO: clean up as part of REVEM-199 (START)
|
||||
if PROGRAM_INFO_FLAG.is_enabled():
|
||||
programs = get_programs(course=course.id)
|
||||
if programs:
|
||||
# A course can be in multiple programs, but we're just grabbing the first one
|
||||
program = programs[0]
|
||||
complete_enrollment = False
|
||||
has_courses_left_to_purchase = False
|
||||
total_courses = None
|
||||
courses = program.get('courses')
|
||||
courses_left_to_purchase_price = None
|
||||
courses_left_to_purchase_url = None
|
||||
program_uuid = program.get('uuid')
|
||||
status = None
|
||||
is_eligible_for_one_click_purchase = None
|
||||
if courses is not None:
|
||||
total_courses = len(courses)
|
||||
complete_enrollment = is_enrolled_in_all_courses(courses, user_enrollments)
|
||||
status = program.get('status')
|
||||
is_eligible_for_one_click_purchase = program.get('is_program_eligible_for_one_click_purchase')
|
||||
# Get the price and purchase URL of the program courses the user has yet to purchase. Say a
|
||||
# program has 3 courses (A, B and C), and the user previously purchased a certificate for A.
|
||||
# The user is enrolled in audit mode for B. The "left to purchase price" should be the price of
|
||||
# B+C.
|
||||
non_audit_enrollments = [en for en in user_enrollments if en not in
|
||||
audit_enrollments]
|
||||
courses_left_to_purchase = get_unenrolled_courses(courses, non_audit_enrollments)
|
||||
if courses_left_to_purchase:
|
||||
has_courses_left_to_purchase = True
|
||||
if is_eligible_for_one_click_purchase:
|
||||
courses_left_to_purchase_price, courses_left_to_purchase_skus = \
|
||||
get_program_price_and_skus(courses_left_to_purchase)
|
||||
if courses_left_to_purchase_skus:
|
||||
courses_left_to_purchase_url = EcommerceService().get_checkout_page_url(
|
||||
*courses_left_to_purchase_skus, program_uuid=program_uuid)
|
||||
|
||||
program_key = {
|
||||
'uuid': program_uuid,
|
||||
'title': program.get('title'),
|
||||
'marketing_url': program.get('marketing_url'),
|
||||
'status': status,
|
||||
'is_eligible_for_one_click_purchase': is_eligible_for_one_click_purchase,
|
||||
'total_courses': total_courses,
|
||||
'complete_enrollment': complete_enrollment,
|
||||
'has_courses_left_to_purchase': has_courses_left_to_purchase,
|
||||
'courses_left_to_purchase_price': courses_left_to_purchase_price,
|
||||
'courses_left_to_purchase_url': courses_left_to_purchase_url,
|
||||
}
|
||||
# TODO: clean up as part of REVEM-199 (END)
|
||||
enrollment = CourseEnrollment.objects.select_related(
|
||||
'course'
|
||||
).get(user_id=user.id, course_id=course.id)
|
||||
if enrollment.is_active:
|
||||
enrollment_mode = enrollment.mode
|
||||
enrollment_time = enrollment.created
|
||||
except CourseEnrollment.DoesNotExist:
|
||||
pass # Not enrolled, used the default None values
|
||||
|
||||
# upgrade_link and upgrade_date should be None if user has passed their dynamic pacing deadline.
|
||||
upgrade_link, upgrade_date = check_and_get_upgrade_link_and_date(user, enrollment, course)
|
||||
has_staff_access = has_staff_access_to_preview_mode(user, course.id)
|
||||
forum_roles = []
|
||||
if user.is_authenticated:
|
||||
forum_roles = list(Role.objects.filter(users=user, course_id=course.id).values_list('name').distinct())
|
||||
|
||||
# get user partition data
|
||||
if user.is_authenticated():
|
||||
partition_groups = get_all_partitions_for_course(course)
|
||||
user_partitions = get_user_partition_groups(course.id, partition_groups, user, 'name')
|
||||
else:
|
||||
user_partitions = {}
|
||||
|
||||
return {
|
||||
'upgrade_link': upgrade_link,
|
||||
'upgrade_price': six.text_type(get_cosmetic_verified_display_price(course)),
|
||||
'enrollment_mode': enrollment_mode,
|
||||
'enrollment_time': enrollment_time,
|
||||
'pacing_type': 'self_paced' if course.self_paced else 'instructor_paced',
|
||||
'upgrade_deadline': upgrade_date,
|
||||
'audit_access_deadline': get_audit_access_expiration(user, course),
|
||||
'course_key': course.id,
|
||||
'course_start': course.start,
|
||||
'course_end': course.end,
|
||||
'has_staff_access': has_staff_access,
|
||||
'forum_roles': forum_roles,
|
||||
'partition_groups': user_partitions,
|
||||
# TODO: clean up as part of REVO-28 (START)
|
||||
'has_non_audit_enrollments': has_non_audit_enrollments,
|
||||
# TODO: clean up as part of REVO-28 (END)
|
||||
# TODO: clean up as part of REVEM-199 (START)
|
||||
'program_key_fields': program_key,
|
||||
# TODO: clean up as part of REVEM-199 (END)
|
||||
}
|
||||
|
||||
|
||||
def get_base_experiment_metadata_context(course, user, enrollment, user_enrollments, audit_enrollments):
|
||||
"""
|
||||
Return a context dictionary with the keys used by dashboard_metadata.html and user_metadata.html
|
||||
@@ -482,12 +329,10 @@ def get_audit_access_expiration(user, course):
|
||||
"""
|
||||
Return the expiration date for the user's audit access to this course.
|
||||
"""
|
||||
if AUDIT_DEADLINE_FLAG.is_enabled():
|
||||
if not CourseDurationLimitConfig.enabled_for_enrollment(user=user, course_key=course.id):
|
||||
return None
|
||||
if not CourseDurationLimitConfig.enabled_for_enrollment(user=user, course_key=course.id):
|
||||
return None
|
||||
|
||||
return get_user_course_expiration_date(user, course)
|
||||
return None
|
||||
return get_user_course_expiration_date(user, course)
|
||||
|
||||
|
||||
# TODO: clean up as part of REVEM-199 (START)
|
||||
|
||||
@@ -39,7 +39,7 @@ from shoppingcart.reports import (
|
||||
UniversityRevenueShareReport
|
||||
)
|
||||
from student.models import AlreadyEnrolledError, CourseEnrollment, CourseFullError, EnrollmentClosedError
|
||||
from util.bad_request_rate_limiter import BadRequestRateLimiter
|
||||
from util.request_rate_limiter import BadRequestRateLimiter
|
||||
from util.date_utils import get_default_time_display
|
||||
from util.json_request import JsonResponse
|
||||
|
||||
@@ -319,7 +319,7 @@ def get_reg_code_validity(registration_code, request, limiter):
|
||||
if not reg_code_is_valid:
|
||||
# tick the rate limiter counter
|
||||
AUDIT_LOG.info(u"Redemption of a invalid RegistrationCode %s", registration_code)
|
||||
limiter.tick_bad_request_counter(request)
|
||||
limiter.tick_request_counter(request)
|
||||
raise Http404()
|
||||
|
||||
return reg_code_is_valid, reg_code_already_redeemed, course_registration
|
||||
|
||||
@@ -60,6 +60,12 @@ CAPTURE_CONSOLE_LOG = True
|
||||
PLATFORM_NAME = ugettext_lazy(u"édX")
|
||||
PLATFORM_DESCRIPTION = ugettext_lazy(u"Open édX Platform")
|
||||
|
||||
# We need to test different scenarios, following setting effectively disbale rate limiting
|
||||
PASSWORD_RESET_EMAIL_RATE_LIMIT = {
|
||||
'no_of_emails': 1,
|
||||
'per_seconds': 1
|
||||
}
|
||||
|
||||
############################ STATIC FILES #############################
|
||||
|
||||
# Enable debug so that static assets are served by Django
|
||||
|
||||
@@ -445,7 +445,10 @@ XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds
|
||||
# Used with Email sending
|
||||
RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS = 5
|
||||
RETRY_ACTIVATION_EMAIL_TIMEOUT = 0.5
|
||||
|
||||
PASSWORD_RESET_EMAIL_RATE_LIMIT = {
|
||||
'no_of_emails': 1,
|
||||
'per_seconds': 60
|
||||
}
|
||||
# Deadline message configurations
|
||||
COURSE_MESSAGE_ALERT_DURATION_IN_DAYS = 14
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
successTitle = gettext('Check Your Email'),
|
||||
successMessageHtml = HtmlUtils.interpolateHtml(
|
||||
gettext('{paragraphStart}You entered {boldStart}{email}{boldEnd}. If this email address is associated with your {platform_name} account, we will send a message with password recovery instructions to this email address.{paragraphEnd}' + // eslint-disable-line max-len
|
||||
'{paragraphStart}If you do not receive a password reset message, verify that you entered the correct email address, or check your spam folder.{paragraphEnd}' + // eslint-disable-line max-len
|
||||
'{paragraphStart}If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder.{paragraphEnd}' + // eslint-disable-line max-len
|
||||
'{paragraphStart}If you need further assistance, {anchorStart}contact technical support{anchorEnd}.{paragraphEnd}'), { // eslint-disable-line max-len
|
||||
boldStart: HtmlUtils.HTML('<b>'),
|
||||
boldEnd: HtmlUtils.HTML('</b>'),
|
||||
|
||||
@@ -170,7 +170,7 @@ from six import string_types
|
||||
href_class = "register"
|
||||
%>
|
||||
<a href="${reg_href}" class="${href_class}">
|
||||
${_("Enroll in {course_name}").format(course_name=course.display_number_with_default)}
|
||||
${_("Enroll Now")}
|
||||
</a>
|
||||
<div id="register_error"></div>
|
||||
%endif
|
||||
|
||||
@@ -8,7 +8,9 @@ if and only if the service is deployed in the Open edX installation.
|
||||
To ensure maximum separation of concerns, and a minimum of interdependencies,
|
||||
this package should be kept small, thin, and stateless.
|
||||
"""
|
||||
from openedx.core.djangoapps.waffle_utils import (WaffleSwitch, WaffleSwitchNamespace)
|
||||
from __future__ import absolute_import
|
||||
|
||||
from openedx.core.djangoapps.waffle_utils import WaffleSwitch, WaffleSwitchNamespace
|
||||
|
||||
PROGRAMS_WAFFLE_SWITCH_NAMESPACE = WaffleSwitchNamespace(name='programs')
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""
|
||||
django admin pages for program support models
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from config_models.admin import ConfigurationModelAdmin
|
||||
from django.contrib import admin
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""
|
||||
Programs Configuration
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Models providing Programs support for the LMS and Studio."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from config_models.models import ConfigurationModel
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""
|
||||
This module contains signals / handlers related to programs.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
|
||||
from django.dispatch import receiver
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Helper functions for working with Programs."""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from itertools import chain
|
||||
from urlparse import urljoin, urlparse, urlunparse
|
||||
|
||||
import six
|
||||
from six.moves.urllib.parse import urljoin, urlparse, urlunparse # pylint: disable=import-error
|
||||
from dateutil.parser import parse
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
@@ -25,7 +28,7 @@ from lms.djangoapps.certificates.models import GeneratedCertificate
|
||||
from lms.djangoapps.commerce.utils import EcommerceService
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.grades.api import CourseGradeFactory
|
||||
from openedx.core.djangoapps.catalog.utils import get_programs, get_fulfillable_course_runs_for_entitlement
|
||||
from openedx.core.djangoapps.catalog.utils import get_fulfillable_course_runs_for_entitlement, get_programs
|
||||
from openedx.core.djangoapps.certificates.api import available_date_for_certificate
|
||||
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
@@ -95,7 +98,7 @@ class ProgramProgressMeter(object):
|
||||
self.course_run_ids = []
|
||||
for enrollment in self.enrollments:
|
||||
# enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻
|
||||
enrollment_id = unicode(enrollment.course_id)
|
||||
enrollment_id = six.text_type(enrollment.course_id)
|
||||
mode = enrollment.mode
|
||||
if mode == CourseMode.NO_ID_PROFESSIONAL_MODE:
|
||||
mode = CourseMode.PROFESSIONAL
|
||||
@@ -140,7 +143,7 @@ class ProgramProgressMeter(object):
|
||||
program_list.append(program)
|
||||
|
||||
# Sort programs by title for consistent presentation.
|
||||
for program_list in inverted_programs.itervalues():
|
||||
for program_list in six.itervalues(inverted_programs):
|
||||
program_list.sort(key=lambda p: p['title'])
|
||||
|
||||
return inverted_programs
|
||||
@@ -326,14 +329,16 @@ class ProgramProgressMeter(object):
|
||||
if modes_match and certificate_api.is_passing_status(certificate.status):
|
||||
course_overview = CourseOverview.get_from_id(key)
|
||||
available_date = available_date_for_certificate(course_overview, certificate)
|
||||
earliest_course_run_date = min(filter(None, [available_date, earliest_course_run_date]))
|
||||
earliest_course_run_date = min(
|
||||
[date for date in [available_date, earliest_course_run_date] if date]
|
||||
)
|
||||
|
||||
# If we're missing a cert for a course, the program isn't completed and we should just bail now
|
||||
if earliest_course_run_date is None:
|
||||
return None
|
||||
|
||||
# Keep the catalog course date if it's the latest one
|
||||
program_available_date = max(filter(None, [earliest_course_run_date, program_available_date]))
|
||||
program_available_date = max([date for date in [earliest_course_run_date, program_available_date] if date])
|
||||
|
||||
return program_available_date
|
||||
|
||||
@@ -427,7 +432,7 @@ class ProgramProgressMeter(object):
|
||||
completed_runs, failed_runs = [], []
|
||||
for certificate in course_run_certificates:
|
||||
course_data = {
|
||||
'course_run_id': unicode(certificate['course_key']),
|
||||
'course_run_id': six.text_type(certificate['course_key']),
|
||||
'type': self._certificate_mode_translation(certificate['type']),
|
||||
}
|
||||
|
||||
@@ -592,7 +597,7 @@ class ProgramDataExtender(object):
|
||||
# Here we check the entitlements' expired_at_datetime property rather than filter by the expired_at attribute
|
||||
# to ensure that the expiration status is as up to date as possible
|
||||
entitlements = [e for e in entitlements if not e.expired_at_datetime]
|
||||
courses_with_entitlements = set(unicode(entitlement.course_uuid) for entitlement in entitlements)
|
||||
courses_with_entitlements = set(six.text_type(entitlement.course_uuid) for entitlement in entitlements)
|
||||
return [course for course in courses if course['uuid'] not in courses_with_entitlements]
|
||||
|
||||
def _filter_out_courses_with_enrollments(self, courses):
|
||||
@@ -610,10 +615,10 @@ class ProgramDataExtender(object):
|
||||
is_active=True,
|
||||
mode__in=self.data['applicable_seat_types']
|
||||
)
|
||||
course_runs_with_enrollments = set(unicode(enrollment.course_id) for enrollment in enrollments)
|
||||
course_runs_with_enrollments = set(six.text_type(enrollment.course_id) for enrollment in enrollments)
|
||||
courses_without_enrollments = []
|
||||
for course in courses:
|
||||
if all(unicode(run['key']) not in course_runs_with_enrollments for run in course['course_runs']):
|
||||
if all(six.text_type(run['key']) not in course_runs_with_enrollments for run in course['course_runs']):
|
||||
courses_without_enrollments.append(course)
|
||||
|
||||
return courses_without_enrollments
|
||||
@@ -805,7 +810,7 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
|
||||
self.data['instructor_ordering'] = []
|
||||
|
||||
sorted_instructor_names = [
|
||||
' '.join(filter(None, (instructor['given_name'], instructor['family_name'])))
|
||||
' '.join([name for name in (instructor['given_name'], instructor['family_name']) if name])
|
||||
for instructor in self.data['instructor_ordering']
|
||||
]
|
||||
instructors_to_be_sorted = [
|
||||
|
||||
@@ -12,6 +12,8 @@ import attr
|
||||
import ddt
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Max
|
||||
from edx_ace.channel import ChannelMap, ChannelType
|
||||
from edx_ace.test_utils import StubPolicy, patch_policies
|
||||
from edx_ace.utils.date import serialize
|
||||
@@ -105,6 +107,18 @@ class ScheduleSendEmailTestMixin(FilteredQueryCountMixin):
|
||||
def _calculate_bin_for_user(self, user):
|
||||
return user.id % self.task.num_bins
|
||||
|
||||
def _next_user_id(self):
|
||||
"""
|
||||
Get the next user ID which is a multiple of the bin count and greater
|
||||
than the current largest user ID. Avoids intermittent ID collisions
|
||||
with the user created in ModuleStoreTestCase.setUp().
|
||||
"""
|
||||
max_user_id = User.objects.aggregate(Max('id'))['id__max']
|
||||
if max_user_id is None:
|
||||
max_user_id = 0
|
||||
num_bins = self.task.num_bins
|
||||
return max_user_id + num_bins - (max_user_id % num_bins)
|
||||
|
||||
def _get_dates(self, offset=None):
|
||||
current_day = _get_datetime_beginning_of_day(datetime.datetime.now(pytz.UTC))
|
||||
offset = offset or self.expected_offsets[0]
|
||||
@@ -296,8 +310,8 @@ class ScheduleSendEmailTestMixin(FilteredQueryCountMixin):
|
||||
for config in (this_config, other_config):
|
||||
ScheduleConfigFactory.create(site=config.site)
|
||||
|
||||
user1 = UserFactory.create(id=self.task.num_bins)
|
||||
user2 = UserFactory.create(id=self.task.num_bins * 2)
|
||||
user1 = UserFactory.create(id=self._next_user_id())
|
||||
user2 = UserFactory.create(id=user1.id + self.task.num_bins)
|
||||
current_day, offset, target_day, upgrade_deadline = self._get_dates()
|
||||
|
||||
self._schedule_factory(
|
||||
@@ -323,7 +337,7 @@ class ScheduleSendEmailTestMixin(FilteredQueryCountMixin):
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_course_end(self, has_course_ended):
|
||||
user1 = UserFactory.create(id=self.task.num_bins)
|
||||
user1 = UserFactory.create(id=self._next_user_id())
|
||||
current_day, offset, target_day, upgrade_deadline = self._get_dates()
|
||||
|
||||
end_date_offset = -2 if has_course_ended else 2
|
||||
|
||||
@@ -68,7 +68,6 @@ class UserAccountUpdateTest(CacheIsolationTestCase, UrlResetMixin):
|
||||
OLD_EMAIL = u"walter@graymattertech.com"
|
||||
NEW_EMAIL = u"walt@savewalterwhite.com"
|
||||
|
||||
INVALID_ATTEMPTS = 100
|
||||
INVALID_KEY = u"123abc"
|
||||
|
||||
URLCONF_MODULES = ['student_accounts.urls']
|
||||
@@ -238,17 +237,23 @@ class UserAccountUpdateTest(CacheIsolationTestCase, UrlResetMixin):
|
||||
logger.check((LOGGER_NAME, 'INFO', 'Invalid password reset attempt'))
|
||||
|
||||
def test_password_change_rate_limited(self):
|
||||
"""
|
||||
Tests that consective password reset requests are rate limited.
|
||||
"""
|
||||
# Log out the user created during test setup, to prevent the view from
|
||||
# selecting the logged-in user's email address over the email provided
|
||||
# in the POST data
|
||||
self.client.logout()
|
||||
for status in [200, 403]:
|
||||
response = self._change_password(email=self.NEW_EMAIL)
|
||||
self.assertEqual(response.status_code, status)
|
||||
|
||||
# Make many consecutive bad requests in an attempt to trigger the rate limiter
|
||||
for __ in range(self.INVALID_ATTEMPTS):
|
||||
self._change_password(email=self.NEW_EMAIL)
|
||||
|
||||
response = self._change_password(email=self.NEW_EMAIL)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
with mock.patch(
|
||||
'util.request_rate_limiter.PasswordResetEmailRateLimiter.is_rate_limit_exceeded',
|
||||
return_value=False
|
||||
):
|
||||
response = self._change_password(email=self.NEW_EMAIL)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@ddt.data(
|
||||
('post', 'password_change_request', []),
|
||||
|
||||
@@ -96,6 +96,7 @@ docopt==0.6.2
|
||||
docutils==0.14 # via botocore
|
||||
edx-ace==0.1.10
|
||||
edx-analytics-data-api-client==0.15.3
|
||||
git+https://github.com/edx/edx-bulk-grades@f58f9fa01d95c01211fc94ebb92f0fb1a04bf32b#egg=edx-bulk-grades==0.1.1
|
||||
edx-ccx-keys==0.2.1
|
||||
edx-celeryutils==0.2.7
|
||||
edx-completion==2.0.0
|
||||
@@ -104,7 +105,7 @@ edx-django-release-util==0.3.1
|
||||
edx-django-sites-extensions==2.3.1
|
||||
edx-django-utils==1.0.3
|
||||
edx-drf-extensions==2.3.1
|
||||
edx-enterprise==1.6.5
|
||||
edx-enterprise==1.6.7
|
||||
edx-i18n-tools==0.4.8
|
||||
edx-milestones==0.2.2
|
||||
edx-oauth2-provider==1.2.2
|
||||
@@ -130,7 +131,7 @@ fs==2.0.18
|
||||
future==0.17.1 # via pyjwkest
|
||||
futures==3.2.0 ; python_version == "2.7" # via django-pipeline, python-swiftclient, s3transfer, xblock-utils
|
||||
geoip2==2.9.0
|
||||
glob2==0.6
|
||||
glob2==0.7
|
||||
gunicorn==19.5.0
|
||||
help-tokens==1.0.3
|
||||
html5lib==1.0.1
|
||||
@@ -227,7 +228,9 @@ sorl-thumbnail==12.3
|
||||
sortedcontainers==2.1.0
|
||||
soupsieve==1.9.1 # via beautifulsoup4
|
||||
sqlparse==0.3.0
|
||||
git+https://github.com/edx/staff_graded-xblock.git@a1ebad3e379555617bcbd3ceaee65294e2c93c4c#egg=staff_graded-xblock==0.1
|
||||
stevedore==1.30.1
|
||||
git+https://github.com/edx/super-csv@1a9dcc89eb65c3ed434b509b84062f8fcabd901b#egg=super-csv==0.2
|
||||
sympy==1.4
|
||||
tincan==0.0.5 # via edx-enterprise
|
||||
unicodecsv==0.14.1
|
||||
|
||||
@@ -116,6 +116,7 @@ docopt==0.6.2
|
||||
docutils==0.14
|
||||
edx-ace==0.1.10
|
||||
edx-analytics-data-api-client==0.15.3
|
||||
git+https://github.com/edx/edx-bulk-grades@f58f9fa01d95c01211fc94ebb92f0fb1a04bf32b#egg=edx-bulk-grades==0.1.1
|
||||
edx-ccx-keys==0.2.1
|
||||
edx-celeryutils==0.2.7
|
||||
edx-completion==2.0.0
|
||||
@@ -124,7 +125,7 @@ edx-django-release-util==0.3.1
|
||||
edx-django-sites-extensions==2.3.1
|
||||
edx-django-utils==1.0.3
|
||||
edx-drf-extensions==2.3.1
|
||||
edx-enterprise==1.6.5
|
||||
edx-enterprise==1.6.7
|
||||
edx-i18n-tools==0.4.8
|
||||
edx-lint==1.3.0
|
||||
edx-milestones==0.2.2
|
||||
@@ -163,7 +164,7 @@ functools32==3.2.3.post2 ; python_version == "2.7"
|
||||
future==0.17.1
|
||||
futures==3.2.0 ; python_version == "2.7"
|
||||
geoip2==2.9.0
|
||||
glob2==0.6
|
||||
glob2==0.7
|
||||
gunicorn==19.5.0
|
||||
help-tokens==1.0.3
|
||||
html5lib==1.0.1
|
||||
@@ -302,9 +303,11 @@ soupsieve==1.9.1
|
||||
sphinx==1.8.5
|
||||
sphinxcontrib-websupport==1.1.2 # via sphinx
|
||||
sqlparse==0.3.0
|
||||
git+https://github.com/edx/staff_graded-xblock.git@a1ebad3e379555617bcbd3ceaee65294e2c93c4c#egg=staff_graded-xblock==0.1
|
||||
stevedore==1.30.1
|
||||
git+https://github.com/edx/super-csv@1a9dcc89eb65c3ed434b509b84062f8fcabd901b#egg=super-csv==0.2
|
||||
sympy==1.4
|
||||
testfixtures==6.8.2
|
||||
testfixtures==6.9.0
|
||||
text-unidecode==1.2
|
||||
tincan==0.0.5
|
||||
toml==0.10.0
|
||||
|
||||
@@ -87,6 +87,10 @@ git+https://github.com/edx/crowdsourcehinter.git@518605f0a95190949fe77bd39158450
|
||||
-e git+https://github.com/edx/DoneXBlock.git@01a14f3bd80ae47dd08cdbbe2f88f3eb88d00fba#egg=done-xblock
|
||||
-e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive
|
||||
git+https://github.com/edx/xblock-lti-consumer.git@v1.1.8#egg=lti_consumer-xblock==1.1.8
|
||||
git+https://github.com/edx/super-csv@1a9dcc89eb65c3ed434b509b84062f8fcabd901b#egg=super-csv==0.2
|
||||
git+https://github.com/edx/edx-bulk-grades@f58f9fa01d95c01211fc94ebb92f0fb1a04bf32b#egg=edx-bulk-grades==0.1.1
|
||||
git+https://github.com/edx/staff_graded-xblock.git@a1ebad3e379555617bcbd3ceaee65294e2c93c4c#egg=staff_graded-xblock==0.1
|
||||
|
||||
|
||||
# Third Party XBlocks
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ docopt==0.6.2
|
||||
docutils==0.14
|
||||
edx-ace==0.1.10
|
||||
edx-analytics-data-api-client==0.15.3
|
||||
git+https://github.com/edx/edx-bulk-grades@f58f9fa01d95c01211fc94ebb92f0fb1a04bf32b#egg=edx-bulk-grades==0.1.1
|
||||
edx-ccx-keys==0.2.1
|
||||
edx-celeryutils==0.2.7
|
||||
edx-completion==2.0.0
|
||||
@@ -120,7 +121,7 @@ edx-django-release-util==0.3.1
|
||||
edx-django-sites-extensions==2.3.1
|
||||
edx-django-utils==1.0.3
|
||||
edx-drf-extensions==2.3.1
|
||||
edx-enterprise==1.6.5
|
||||
edx-enterprise==1.6.7
|
||||
edx-i18n-tools==0.4.8
|
||||
edx-lint==1.3.0
|
||||
edx-milestones==0.2.2
|
||||
@@ -158,7 +159,7 @@ functools32==3.2.3.post2 ; python_version == "2.7" # via flake8
|
||||
future==0.17.1
|
||||
futures==3.2.0 ; python_version == "2.7"
|
||||
geoip2==2.9.0
|
||||
glob2==0.6
|
||||
glob2==0.7
|
||||
gunicorn==19.5.0
|
||||
help-tokens==1.0.3
|
||||
html5lib==1.0.1
|
||||
@@ -289,9 +290,11 @@ sorl-thumbnail==12.3
|
||||
sortedcontainers==2.1.0
|
||||
soupsieve==1.9.1
|
||||
sqlparse==0.3.0
|
||||
git+https://github.com/edx/staff_graded-xblock.git@a1ebad3e379555617bcbd3ceaee65294e2c93c4c#egg=staff_graded-xblock==0.1
|
||||
stevedore==1.30.1
|
||||
git+https://github.com/edx/super-csv@1a9dcc89eb65c3ed434b509b84062f8fcabd901b#egg=super-csv==0.2
|
||||
sympy==1.4
|
||||
testfixtures==6.8.2
|
||||
testfixtures==6.9.0
|
||||
text-unidecode==1.2 # via faker
|
||||
tincan==0.0.5
|
||||
toml==0.10.0 # via tox
|
||||
|
||||
Reference in New Issue
Block a user