Merge remote-tracking branch 'origin/release' into bbeggs/2015-09-22-release-to-master

This commit is contained in:
Brian Beggs
2015-09-28 20:58:34 -04:00
65 changed files with 1286 additions and 507 deletions

View File

@@ -6,7 +6,7 @@ from django.test.utils import override_settings
from django.test.client import RequestFactory
from django.conf import settings
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.exceptions import PermissionDenied
from cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf
@@ -24,7 +24,7 @@ class CrossDomainAuthTest(TestCase):
def test_perform_csrf_referer_check(self):
request = self._fake_request()
with self.assertRaisesRegexp(AuthenticationFailed, 'CSRF'):
with self.assertRaisesRegexp(PermissionDenied, 'CSRF'):
self.auth.enforce_csrf(request)
@patch.dict(settings.FEATURES, {

View File

@@ -11,7 +11,7 @@ from enrollment.errors import (
CourseNotFoundError, CourseEnrollmentClosedError, CourseEnrollmentFullError,
CourseEnrollmentExistsError, UserNotFoundError, InvalidEnrollmentAttribute
)
from enrollment.serializers import CourseEnrollmentSerializer, CourseField
from enrollment.serializers import CourseEnrollmentSerializer, CourseSerializer
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from student.models import (
CourseEnrollment, NonExistentCourseError, EnrollmentClosedError,
@@ -35,9 +35,30 @@ def get_course_enrollments(user_id):
"""
qset = CourseEnrollment.objects.filter(
user__username=user_id, is_active=True
user__username=user_id,
is_active=True
).order_by('created')
return CourseEnrollmentSerializer(qset).data
enrollments = CourseEnrollmentSerializer(qset, many=True).data
# Find deleted courses and filter them out of the results
deleted = []
valid = []
for enrollment in enrollments:
if enrollment.get("course_details") is not None:
valid.append(enrollment)
else:
deleted.append(enrollment)
if deleted:
log.warning(
(
u"Course enrollments for user %s reference "
u"courses that do not exist (this can occur if a course is deleted)."
), user_id,
)
return valid
def get_course_enrollment(username, course_id):
@@ -271,4 +292,4 @@ def get_course_enrollment_info(course_id, include_expired=False):
log.warning(msg)
raise CourseNotFoundError(msg)
else:
return CourseField().to_native(course, include_expired=include_expired)
return CourseSerializer(course, include_expired=include_expired).data

View File

@@ -30,32 +30,36 @@ class StringListField(serializers.CharField):
return [int(item) for item in items]
class CourseField(serializers.RelatedField):
"""Read-Only representation of course enrollment information.
Aggregates course information from the CourseDescriptor as well as the Course Modes configured
for enrolling in the course.
class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serialize a course descriptor and related information.
"""
def to_native(self, course, **kwargs):
course_modes = ModeSerializer(
CourseMode.modes_for_course(
course.id,
include_expired=kwargs.get('include_expired', False),
only_selectable=False
)
).data
course_id = serializers.CharField(source="id")
enrollment_start = serializers.DateTimeField(format=None)
enrollment_end = serializers.DateTimeField(format=None)
course_start = serializers.DateTimeField(source="start", format=None)
course_end = serializers.DateTimeField(source="end", format=None)
invite_only = serializers.BooleanField(source="invitation_only")
course_modes = serializers.SerializerMethodField()
return {
'course_id': unicode(course.id),
'enrollment_start': course.enrollment_start,
'enrollment_end': course.enrollment_end,
'course_start': course.start,
'course_end': course.end,
'invite_only': course.invitation_only,
'course_modes': course_modes,
}
def __init__(self, *args, **kwargs):
self.include_expired = kwargs.pop("include_expired", False)
super(CourseSerializer, self).__init__(*args, **kwargs)
def get_course_modes(self, obj):
"""
Retrieve course modes associated with the course.
"""
course_modes = CourseMode.modes_for_course(
obj.id,
include_expired=self.include_expired,
only_selectable=False
)
return [
ModeSerializer(mode).data
for mode in course_modes
]
class CourseEnrollmentSerializer(serializers.ModelSerializer):
@@ -65,34 +69,9 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
the Course Descriptor and course modes, to give a complete representation of course enrollment.
"""
course_details = serializers.SerializerMethodField('get_course_details')
course_details = CourseSerializer(source="course_overview")
user = serializers.SerializerMethodField('get_username')
@property
def data(self):
serialized_data = super(CourseEnrollmentSerializer, self).data
# filter the results with empty courses 'course_details'
if isinstance(serialized_data, dict):
if serialized_data.get('course_details') is None:
return None
return serialized_data
return [enrollment for enrollment in serialized_data if enrollment.get('course_details')]
def get_course_details(self, model):
if model.course is None:
msg = u"Course '{0}' does not exist (maybe deleted), in which User (user_id: '{1}') is enrolled.".format(
model.course_id,
model.user.id
)
log.warning(msg)
return None
field = CourseField()
return field.to_native(model.course)
def get_username(self, model):
"""Retrieves the username from the associated model."""
return model.username

View File

@@ -1038,7 +1038,7 @@ class EnrollmentCrossDomainTest(ModuleStoreTestCase):
@cross_domain_config
def test_cross_domain_missing_csrf(self, *args): # pylint: disable=unused-argument
resp = self._cross_domain_post('invalid_csrf_token')
self.assertEqual(resp.status_code, 401)
self.assertEqual(resp.status_code, 403)
def _get_csrf_cookie(self):
"""Retrieve the cross-domain CSRF cookie. """

View File

@@ -5,10 +5,18 @@ This module requires that :class:`request_cache.middleware.RequestCache`
is installed in order to clear the cache after each request.
"""
import logging
from urlparse import urlparse
from django.conf import settings
from django.test.client import RequestFactory
from request_cache import middleware
log = logging.getLogger(__name__)
def get_cache(name):
"""
Return the request cache named ``name``.
@@ -26,3 +34,38 @@ def get_request():
Return the current request.
"""
return middleware.RequestCache.get_current_request()
def get_request_or_stub():
"""
Return the current request or a stub request.
If called outside the context of a request, construct a fake
request that can be used to build an absolute URI.
This is useful in cases where we need to pass in a request object
but don't have an active request (for example, in test cases).
"""
request = get_request()
if request is None:
log.warning(
"Could not retrieve the current request. "
"A stub request will be created instead using settings.SITE_NAME. "
"This should be used *only* in test cases, never in production!"
)
# The settings SITE_NAME may contain a port number, so we need to
# parse the full URL.
full_url = "http://{site_name}".format(site_name=settings.SITE_NAME)
parsed_url = urlparse(full_url)
# Construct the fake request. This can be used to construct absolute
# URIs to other paths.
return RequestFactory(
SERVER_NAME=parsed_url.hostname,
SERVER_PORT=parsed_url.port or 80,
).get("/")
else:
return request

View File

@@ -0,0 +1,20 @@
"""
Tests for the request cache.
"""
from django.conf import settings
from django.test import TestCase
from request_cache import get_request_or_stub
class TestRequestCache(TestCase):
"""
Tests for the request cache.
"""
def test_get_request_or_stub(self):
# Outside the context of the request, we should still get a request
# that allows us to build an absolute URI.
stub = get_request_or_stub()
expected_url = "http://{site_name}/foobar".format(site_name=settings.SITE_NAME)
self.assertEqual(stub.build_absolute_uri("foobar"), expected_url)

View File

@@ -406,7 +406,7 @@ class BrowseTopicsTest(TeamsTabBase):
)
create_team_page.submit_form()
team_page = TeamPage(self.browser, self.course_id)
self.assertTrue(team_page.is_browser_on_page)
self.assertTrue(team_page.is_browser_on_page())
team_page.click_all_topics()
self.assertTrue(self.topics_page.is_browser_on_page())
self.topics_page.wait_for_ajax()