Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
2fd6add5
Commit
2fd6add5
authored
Sep 28, 2015
by
Ben Patterson
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Revert "Merge DRF 3.1 in to master"
parent
00473d44
Hide whitespace changes
Inline
Side-by-side
Showing
65 changed files
with
500 additions
and
1282 deletions
+500
-1282
cms/envs/common.py
+1
-4
common/djangoapps/cors_csrf/tests/test_authentication.py
+2
-2
common/djangoapps/enrollment/data.py
+4
-25
common/djangoapps/enrollment/serializers.py
+49
-28
common/djangoapps/enrollment/tests/test_views.py
+1
-1
common/djangoapps/request_cache/__init__.py
+0
-43
common/djangoapps/request_cache/tests.py
+0
-20
common/test/acceptance/tests/lms/test_teams.py
+1
-1
lms/djangoapps/commerce/api/v1/serializers.py
+8
-32
lms/djangoapps/commerce/api/v1/views.py
+3
-11
lms/djangoapps/course_structure_api/v0/serializers.py
+5
-5
lms/djangoapps/course_structure_api/v0/tests.py
+16
-67
lms/djangoapps/course_structure_api/v0/views.py
+5
-2
lms/djangoapps/courseware/grades.py
+1
-10
lms/djangoapps/discussion_api/api.py
+2
-2
lms/djangoapps/discussion_api/pagination.py
+14
-23
lms/djangoapps/discussion_api/serializers.py
+54
-90
lms/djangoapps/discussion_api/tests/test_api.py
+3
-4
lms/djangoapps/discussion_api/tests/test_serializers.py
+4
-9
lms/djangoapps/discussion_api/tests/test_views.py
+3
-3
lms/djangoapps/discussion_api/views.py
+1
-2
lms/djangoapps/django_comment_client/base/tests.py
+2
-1
lms/djangoapps/django_comment_client/forum/tests.py
+0
-3
lms/djangoapps/django_comment_client/tests/group_id.py
+0
-2
lms/djangoapps/mobile_api/social_facebook/courses/views.py
+2
-3
lms/djangoapps/mobile_api/social_facebook/friends/views.py
+1
-1
lms/djangoapps/mobile_api/social_facebook/groups/views.py
+3
-3
lms/djangoapps/mobile_api/social_facebook/preferences/serializers.py
+1
-1
lms/djangoapps/mobile_api/social_facebook/preferences/views.py
+2
-2
lms/djangoapps/mobile_api/social_facebook/utils.py
+1
-1
lms/djangoapps/mobile_api/users/serializers.py
+4
-4
lms/djangoapps/mobile_api/users/views.py
+0
-8
lms/djangoapps/notifier_api/serializers.py
+3
-3
lms/djangoapps/notifier_api/views.py
+2
-22
lms/djangoapps/teams/models.py
+1
-0
lms/djangoapps/teams/search_indexes.py
+1
-10
lms/djangoapps/teams/serializers.py
+37
-57
lms/djangoapps/teams/tests/factories.py
+0
-7
lms/djangoapps/teams/tests/test_serializers.py
+43
-44
lms/djangoapps/teams/tests/test_views.py
+7
-55
lms/djangoapps/teams/views.py
+56
-124
lms/envs/common.py
+0
-14
lms/startup.py
+4
-5
openedx/core/djangoapps/content/course_structures/api/v0/api.py
+1
-1
openedx/core/djangoapps/content/course_structures/api/v0/serializers.py
+3
-40
openedx/core/djangoapps/credit/serializers.py
+0
-21
openedx/core/djangoapps/credit/tests/test_views.py
+4
-1
openedx/core/djangoapps/credit/views.py
+8
-20
openedx/core/djangoapps/profile_images/tests/test_views.py
+0
-38
openedx/core/djangoapps/user_api/accounts/api.py
+1
-1
openedx/core/djangoapps/user_api/accounts/serializers.py
+17
-39
openedx/core/djangoapps/user_api/accounts/tests/test_api.py
+1
-4
openedx/core/djangoapps/user_api/accounts/tests/test_views.py
+9
-12
openedx/core/djangoapps/user_api/preferences/api.py
+3
-14
openedx/core/djangoapps/user_api/preferences/tests/test_api.py
+1
-1
openedx/core/djangoapps/user_api/serializers.py
+4
-13
openedx/core/lib/api/authentication.py
+4
-5
openedx/core/lib/api/fields.py
+22
-14
openedx/core/lib/api/mixins.py
+0
-33
openedx/core/lib/api/paginators.py
+0
-25
openedx/core/lib/api/serializers.py
+29
-6
openedx/core/lib/api/tests/test_authentication.py
+42
-214
openedx/core/lib/api/view_utils.py
+0
-21
requirements/edx/base.txt
+1
-1
requirements/edx/github.txt
+3
-4
No files found.
cms/envs/common.py
View file @
2fd6add5
...
@@ -44,10 +44,7 @@ from lms.envs.common import (
...
@@ -44,10 +44,7 @@ from lms.envs.common import (
PROFILE_IMAGE_SECRET_KEY
,
PROFILE_IMAGE_MIN_BYTES
,
PROFILE_IMAGE_MAX_BYTES
,
PROFILE_IMAGE_SECRET_KEY
,
PROFILE_IMAGE_MIN_BYTES
,
PROFILE_IMAGE_MAX_BYTES
,
# The following setting is included as it is used to check whether to
# The following setting is included as it is used to check whether to
# display credit eligibility table on the CMS or not.
# display credit eligibility table on the CMS or not.
ENABLE_CREDIT_ELIGIBILITY
,
YOUTUBE_API_KEY
,
ENABLE_CREDIT_ELIGIBILITY
,
YOUTUBE_API_KEY
# Django REST framework configuration
REST_FRAMEWORK
,
)
)
from
path
import
Path
as
path
from
path
import
Path
as
path
from
warnings
import
simplefilter
from
warnings
import
simplefilter
...
...
common/djangoapps/cors_csrf/tests/test_authentication.py
View file @
2fd6add5
...
@@ -6,7 +6,7 @@ from django.test.utils import override_settings
...
@@ -6,7 +6,7 @@ from django.test.utils import override_settings
from
django.test.client
import
RequestFactory
from
django.test.client
import
RequestFactory
from
django.conf
import
settings
from
django.conf
import
settings
from
rest_framework.exceptions
import
PermissionDeni
ed
from
rest_framework.exceptions
import
AuthenticationFail
ed
from
cors_csrf.authentication
import
SessionAuthenticationCrossDomainCsrf
from
cors_csrf.authentication
import
SessionAuthenticationCrossDomainCsrf
...
@@ -24,7 +24,7 @@ class CrossDomainAuthTest(TestCase):
...
@@ -24,7 +24,7 @@ class CrossDomainAuthTest(TestCase):
def
test_perform_csrf_referer_check
(
self
):
def
test_perform_csrf_referer_check
(
self
):
request
=
self
.
_fake_request
()
request
=
self
.
_fake_request
()
with
self
.
assertRaisesRegexp
(
PermissionDeni
ed
,
'CSRF'
):
with
self
.
assertRaisesRegexp
(
AuthenticationFail
ed
,
'CSRF'
):
self
.
auth
.
enforce_csrf
(
request
)
self
.
auth
.
enforce_csrf
(
request
)
@patch.dict
(
settings
.
FEATURES
,
{
@patch.dict
(
settings
.
FEATURES
,
{
...
...
common/djangoapps/enrollment/data.py
View file @
2fd6add5
...
@@ -11,7 +11,7 @@ from enrollment.errors import (
...
@@ -11,7 +11,7 @@ from enrollment.errors import (
CourseNotFoundError
,
CourseEnrollmentClosedError
,
CourseEnrollmentFullError
,
CourseNotFoundError
,
CourseEnrollmentClosedError
,
CourseEnrollmentFullError
,
CourseEnrollmentExistsError
,
UserNotFoundError
,
InvalidEnrollmentAttribute
CourseEnrollmentExistsError
,
UserNotFoundError
,
InvalidEnrollmentAttribute
)
)
from
enrollment.serializers
import
CourseEnrollmentSerializer
,
Course
Serializer
from
enrollment.serializers
import
CourseEnrollmentSerializer
,
Course
Field
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
student.models
import
(
from
student.models
import
(
CourseEnrollment
,
NonExistentCourseError
,
EnrollmentClosedError
,
CourseEnrollment
,
NonExistentCourseError
,
EnrollmentClosedError
,
...
@@ -35,30 +35,9 @@ def get_course_enrollments(user_id):
...
@@ -35,30 +35,9 @@ def get_course_enrollments(user_id):
"""
"""
qset
=
CourseEnrollment
.
objects
.
filter
(
qset
=
CourseEnrollment
.
objects
.
filter
(
user__username
=
user_id
,
user__username
=
user_id
,
is_active
=
True
is_active
=
True
)
.
order_by
(
'created'
)
)
.
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
):
def
get_course_enrollment
(
username
,
course_id
):
...
@@ -292,4 +271,4 @@ def get_course_enrollment_info(course_id, include_expired=False):
...
@@ -292,4 +271,4 @@ def get_course_enrollment_info(course_id, include_expired=False):
log
.
warning
(
msg
)
log
.
warning
(
msg
)
raise
CourseNotFoundError
(
msg
)
raise
CourseNotFoundError
(
msg
)
else
:
else
:
return
Course
Serializer
(
course
,
include_expired
=
include_expired
)
.
data
return
Course
Field
()
.
to_native
(
course
,
include_expired
=
include_expired
)
common/djangoapps/enrollment/serializers.py
View file @
2fd6add5
...
@@ -30,36 +30,32 @@ class StringListField(serializers.CharField):
...
@@ -30,36 +30,32 @@ class StringListField(serializers.CharField):
return
[
int
(
item
)
for
item
in
items
]
return
[
int
(
item
)
for
item
in
items
]
class
CourseSerializer
(
serializers
.
Serializer
):
# pylint: disable=abstract-method
class
CourseField
(
serializers
.
RelatedField
):
"""
"""Read-Only representation of course enrollment information.
Serialize a course descriptor and related information.
"""
course_id
=
serializers
.
CharField
(
source
=
"id"
)
Aggregates course information from the CourseDescriptor as well as the Course Modes configured
enrollment_start
=
serializers
.
DateTimeField
(
format
=
None
)
for enrolling in the course.
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
()
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
):
def
to_native
(
self
,
course
,
**
kwargs
):
"""
course_modes
=
ModeSerializer
(
Retrieve course modes associated with the course.
CourseMode
.
modes_for_course
(
"""
course
.
id
,
course_modes
=
CourseMode
.
modes_for_course
(
include_expired
=
kwargs
.
get
(
'include_expired'
,
False
),
obj
.
id
,
only_selectable
=
False
include_expired
=
self
.
include_expired
,
)
only_selectable
=
False
)
.
data
)
return
[
return
{
ModeSerializer
(
mode
)
.
data
'course_id'
:
unicode
(
course
.
id
),
for
mode
in
course_modes
'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
,
}
class
CourseEnrollmentSerializer
(
serializers
.
ModelSerializer
):
class
CourseEnrollmentSerializer
(
serializers
.
ModelSerializer
):
...
@@ -69,9 +65,34 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
...
@@ -69,9 +65,34 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
the Course Descriptor and course modes, to give a complete representation of course enrollment.
the Course Descriptor and course modes, to give a complete representation of course enrollment.
"""
"""
course_details
=
CourseSerializer
(
source
=
"course_overview"
)
course_details
=
serializers
.
SerializerMethodField
(
'get_course_details'
)
user
=
serializers
.
SerializerMethodField
(
'get_username'
)
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
):
def
get_username
(
self
,
model
):
"""Retrieves the username from the associated model."""
"""Retrieves the username from the associated model."""
return
model
.
username
return
model
.
username
...
...
common/djangoapps/enrollment/tests/test_views.py
View file @
2fd6add5
...
@@ -1038,7 +1038,7 @@ class EnrollmentCrossDomainTest(ModuleStoreTestCase):
...
@@ -1038,7 +1038,7 @@ class EnrollmentCrossDomainTest(ModuleStoreTestCase):
@cross_domain_config
@cross_domain_config
def
test_cross_domain_missing_csrf
(
self
,
*
args
):
# pylint: disable=unused-argument
def
test_cross_domain_missing_csrf
(
self
,
*
args
):
# pylint: disable=unused-argument
resp
=
self
.
_cross_domain_post
(
'invalid_csrf_token'
)
resp
=
self
.
_cross_domain_post
(
'invalid_csrf_token'
)
self
.
assertEqual
(
resp
.
status_code
,
40
3
)
self
.
assertEqual
(
resp
.
status_code
,
40
1
)
def
_get_csrf_cookie
(
self
):
def
_get_csrf_cookie
(
self
):
"""Retrieve the cross-domain CSRF cookie. """
"""Retrieve the cross-domain CSRF cookie. """
...
...
common/djangoapps/request_cache/__init__.py
View file @
2fd6add5
...
@@ -5,18 +5,10 @@ This module requires that :class:`request_cache.middleware.RequestCache`
...
@@ -5,18 +5,10 @@ This module requires that :class:`request_cache.middleware.RequestCache`
is installed in order to clear the cache after each request.
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
from
request_cache
import
middleware
log
=
logging
.
getLogger
(
__name__
)
def
get_cache
(
name
):
def
get_cache
(
name
):
"""
"""
Return the request cache named ``name``.
Return the request cache named ``name``.
...
@@ -34,38 +26,3 @@ def get_request():
...
@@ -34,38 +26,3 @@ def get_request():
Return the current request.
Return the current request.
"""
"""
return
middleware
.
RequestCache
.
get_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
common/djangoapps/request_cache/tests.py
deleted
100644 → 0
View file @
00473d44
"""
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
)
common/test/acceptance/tests/lms/test_teams.py
View file @
2fd6add5
...
@@ -406,7 +406,7 @@ class BrowseTopicsTest(TeamsTabBase):
...
@@ -406,7 +406,7 @@ class BrowseTopicsTest(TeamsTabBase):
)
)
create_team_page
.
submit_form
()
create_team_page
.
submit_form
()
team_page
=
TeamPage
(
self
.
browser
,
self
.
course_id
)
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
()
team_page
.
click_all_topics
()
self
.
assertTrue
(
self
.
topics_page
.
is_browser_on_page
())
self
.
assertTrue
(
self
.
topics_page
.
is_browser_on_page
())
self
.
topics_page
.
wait_for_ajax
()
self
.
topics_page
.
wait_for_ajax
()
...
...
lms/djangoapps/commerce/api/v1/serializers.py
View file @
2fd6add5
...
@@ -18,12 +18,7 @@ class CourseModeSerializer(serializers.ModelSerializer):
...
@@ -18,12 +18,7 @@ class CourseModeSerializer(serializers.ModelSerializer):
""" CourseMode serializer. """
""" CourseMode serializer. """
name
=
serializers
.
CharField
(
source
=
'mode_slug'
)
name
=
serializers
.
CharField
(
source
=
'mode_slug'
)
price
=
serializers
.
IntegerField
(
source
=
'min_price'
)
price
=
serializers
.
IntegerField
(
source
=
'min_price'
)
expires
=
serializers
.
DateTimeField
(
expires
=
serializers
.
DateTimeField
(
source
=
'expiration_datetime'
,
required
=
False
,
blank
=
True
)
source
=
'expiration_datetime'
,
required
=
False
,
allow_null
=
True
,
format
=
None
)
def
get_identity
(
self
,
data
):
def
get_identity
(
self
,
data
):
try
:
try
:
...
@@ -61,8 +56,8 @@ class CourseSerializer(serializers.Serializer):
...
@@ -61,8 +56,8 @@ class CourseSerializer(serializers.Serializer):
""" Course serializer. """
""" Course serializer. """
id
=
serializers
.
CharField
(
validators
=
[
validate_course_id
])
# pylint: disable=invalid-name
id
=
serializers
.
CharField
(
validators
=
[
validate_course_id
])
# pylint: disable=invalid-name
name
=
serializers
.
CharField
(
read_only
=
True
)
name
=
serializers
.
CharField
(
read_only
=
True
)
verification_deadline
=
serializers
.
DateTimeField
(
format
=
None
,
allow_null
=
True
,
required
=
Fals
e
)
verification_deadline
=
serializers
.
DateTimeField
(
blank
=
Tru
e
)
modes
=
CourseModeSerializer
(
many
=
True
)
modes
=
CourseModeSerializer
(
many
=
True
,
allow_add_remove
=
True
)
def
validate
(
self
,
attrs
):
def
validate
(
self
,
attrs
):
""" Ensure the verification deadline occurs AFTER the course mode enrollment deadlines. """
""" Ensure the verification deadline occurs AFTER the course mode enrollment deadlines. """
...
@@ -73,7 +68,7 @@ class CourseSerializer(serializers.Serializer):
...
@@ -73,7 +68,7 @@ class CourseSerializer(serializers.Serializer):
# Find the earliest upgrade deadline
# Find the earliest upgrade deadline
for
mode
in
attrs
[
'modes'
]:
for
mode
in
attrs
[
'modes'
]:
expires
=
mode
.
get
(
"expiration_datetime"
)
expires
=
mode
.
expiration_datetime
if
expires
:
if
expires
:
# If we don't already have an upgrade_deadline value, use datetime.max so that we can actually
# If we don't already have an upgrade_deadline value, use datetime.max so that we can actually
# complete the comparison.
# complete the comparison.
...
@@ -87,28 +82,9 @@ class CourseSerializer(serializers.Serializer):
...
@@ -87,28 +82,9 @@ class CourseSerializer(serializers.Serializer):
return
attrs
return
attrs
def
create
(
self
,
validated_data
):
def
restore_object
(
self
,
attrs
,
instance
=
None
):
"""Create course modes for a course. """
if
instance
is
None
:
course
=
Course
(
return
Course
(
attrs
[
'id'
],
attrs
[
'modes'
],
attrs
[
'verification_deadline'
])
validated_data
[
"id"
],
self
.
_new_course_mode_models
(
validated_data
[
"modes"
]),
verification_deadline
=
validated_data
[
"verification_deadline"
]
)
course
.
save
()
return
course
def
update
(
self
,
instance
,
validated_data
):
"""Update course modes for an existing course. """
validated_data
[
"modes"
]
=
self
.
_new_course_mode_models
(
validated_data
[
"modes"
])
instance
.
update
(
validated_data
)
instance
.
update
(
attrs
)
instance
.
save
()
return
instance
return
instance
@staticmethod
def
_new_course_mode_models
(
modes_data
):
"""Convert validated course mode data to CourseMode objects. """
return
[
CourseMode
(
**
modes_dict
)
for
modes_dict
in
modes_data
]
lms/djangoapps/commerce/api/v1/views.py
View file @
2fd6add5
...
@@ -2,8 +2,7 @@
...
@@ -2,8 +2,7 @@
import
logging
import
logging
from
django.http
import
Http404
from
django.http
import
Http404
from
rest_framework.authentication
import
SessionAuthentication
from
rest_framework.authentication
import
OAuth2Authentication
,
SessionAuthentication
from
rest_framework_oauth.authentication
import
OAuth2Authentication
from
rest_framework.generics
import
RetrieveUpdateAPIView
,
ListAPIView
from
rest_framework.generics
import
RetrieveUpdateAPIView
,
ListAPIView
from
rest_framework.permissions
import
IsAuthenticated
from
rest_framework.permissions
import
IsAuthenticated
...
@@ -11,7 +10,6 @@ from commerce.api.v1.models import Course
...
@@ -11,7 +10,6 @@ from commerce.api.v1.models import Course
from
commerce.api.v1.permissions
import
ApiKeyOrModelPermission
from
commerce.api.v1.permissions
import
ApiKeyOrModelPermission
from
commerce.api.v1.serializers
import
CourseSerializer
from
commerce.api.v1.serializers
import
CourseSerializer
from
course_modes.models
import
CourseMode
from
course_modes.models
import
CourseMode
from
openedx.core.lib.api.mixins
import
PutAsCreateMixin
log
=
logging
.
getLogger
(
__name__
)
log
=
logging
.
getLogger
(
__name__
)
...
@@ -21,13 +19,12 @@ class CourseListView(ListAPIView):
...
@@ -21,13 +19,12 @@ class CourseListView(ListAPIView):
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
,)
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
,)
permission_classes
=
(
IsAuthenticated
,)
permission_classes
=
(
IsAuthenticated
,)
serializer_class
=
CourseSerializer
serializer_class
=
CourseSerializer
pagination_class
=
None
def
get_queryset
(
self
):
def
get_queryset
(
self
):
return
list
(
Course
.
iterator
()
)
return
Course
.
iterator
(
)
class
CourseRetrieveUpdateView
(
PutAsCreateMixin
,
RetrieveUpdateAPIView
):
class
CourseRetrieveUpdateView
(
RetrieveUpdateAPIView
):
""" Retrieve, update, or create courses/modes. """
""" Retrieve, update, or create courses/modes. """
lookup_field
=
'id'
lookup_field
=
'id'
lookup_url_kwarg
=
'course_id'
lookup_url_kwarg
=
'course_id'
...
@@ -36,11 +33,6 @@ class CourseRetrieveUpdateView(PutAsCreateMixin, RetrieveUpdateAPIView):
...
@@ -36,11 +33,6 @@ class CourseRetrieveUpdateView(PutAsCreateMixin, RetrieveUpdateAPIView):
permission_classes
=
(
ApiKeyOrModelPermission
,)
permission_classes
=
(
ApiKeyOrModelPermission
,)
serializer_class
=
CourseSerializer
serializer_class
=
CourseSerializer
# Django Rest Framework v3 requires that we provide a queryset.
# Note that we're overriding `get_object()` below to return a `Course`
# rather than a CourseMode, so this isn't really used.
queryset
=
CourseMode
.
objects
.
all
()
def
get_object
(
self
,
queryset
=
None
):
def
get_object
(
self
,
queryset
=
None
):
course_id
=
self
.
kwargs
.
get
(
self
.
lookup_url_kwarg
)
course_id
=
self
.
kwargs
.
get
(
self
.
lookup_url_kwarg
)
course
=
Course
.
get
(
course_id
)
course
=
Course
.
get
(
course_id
)
...
...
lms/djangoapps/course_structure_api/v0/serializers.py
View file @
2fd6add5
...
@@ -11,11 +11,11 @@ class CourseSerializer(serializers.Serializer):
...
@@ -11,11 +11,11 @@ class CourseSerializer(serializers.Serializer):
id
=
serializers
.
CharField
()
# pylint: disable=invalid-name
id
=
serializers
.
CharField
()
# pylint: disable=invalid-name
name
=
serializers
.
CharField
(
source
=
'display_name'
)
name
=
serializers
.
CharField
(
source
=
'display_name'
)
category
=
serializers
.
CharField
()
category
=
serializers
.
CharField
()
org
=
serializers
.
SerializerMethodField
()
org
=
serializers
.
SerializerMethodField
(
'get_org'
)
run
=
serializers
.
SerializerMethodField
()
run
=
serializers
.
SerializerMethodField
(
'get_run'
)
course
=
serializers
.
SerializerMethodField
()
course
=
serializers
.
SerializerMethodField
(
'get_course'
)
uri
=
serializers
.
SerializerMethodField
()
uri
=
serializers
.
SerializerMethodField
(
'get_uri'
)
image_url
=
serializers
.
SerializerMethodField
()
image_url
=
serializers
.
SerializerMethodField
(
'get_image_url'
)
start
=
serializers
.
DateTimeField
()
start
=
serializers
.
DateTimeField
()
end
=
serializers
.
DateTimeField
()
end
=
serializers
.
DateTimeField
()
...
...
lms/djangoapps/course_structure_api/v0/tests.py
View file @
2fd6add5
...
@@ -36,23 +36,6 @@ class CourseViewTestsMixin(object):
...
@@ -36,23 +36,6 @@ class CourseViewTestsMixin(object):
"""
"""
view
=
None
view
=
None
raw_grader
=
[
{
"min_count"
:
24
,
"weight"
:
0.2
,
"type"
:
"Homework"
,
"drop_count"
:
0
,
"short_label"
:
"HW"
},
{
"min_count"
:
4
,
"weight"
:
0.8
,
"type"
:
"Exam"
,
"drop_count"
:
0
,
"short_label"
:
"Exam"
}
]
def
setUp
(
self
):
def
setUp
(
self
):
super
(
CourseViewTestsMixin
,
self
)
.
setUp
()
super
(
CourseViewTestsMixin
,
self
)
.
setUp
()
self
.
create_user_and_access_token
()
self
.
create_user_and_access_token
()
...
@@ -68,7 +51,22 @@ class CourseViewTestsMixin(object):
...
@@ -68,7 +51,22 @@ class CourseViewTestsMixin(object):
@classmethod
@classmethod
def
create_course_data
(
cls
):
def
create_course_data
(
cls
):
cls
.
invalid_course_id
=
'foo/bar/baz'
cls
.
invalid_course_id
=
'foo/bar/baz'
cls
.
course
=
CourseFactory
.
create
(
display_name
=
'An Introduction to API Testing'
,
raw_grader
=
cls
.
raw_grader
)
cls
.
course
=
CourseFactory
.
create
(
display_name
=
'An Introduction to API Testing'
,
raw_grader
=
[
{
"min_count"
:
24
,
"weight"
:
0.2
,
"type"
:
"Homework"
,
"drop_count"
:
0
,
"short_label"
:
"HW"
},
{
"min_count"
:
4
,
"weight"
:
0.8
,
"type"
:
"Exam"
,
"drop_count"
:
0
,
"short_label"
:
"Exam"
}
])
cls
.
course_id
=
unicode
(
cls
.
course
.
id
)
cls
.
course_id
=
unicode
(
cls
.
course
.
id
)
with
cls
.
store
.
bulk_operations
(
cls
.
course
.
id
,
emit_signals
=
False
):
with
cls
.
store
.
bulk_operations
(
cls
.
course
.
id
,
emit_signals
=
False
):
cls
.
sequential
=
ItemFactory
.
create
(
cls
.
sequential
=
ItemFactory
.
create
(
...
@@ -410,55 +408,6 @@ class CourseGradingPolicyTests(CourseDetailTestMixin, CourseViewTestsMixin, Shar
...
@@ -410,55 +408,6 @@ class CourseGradingPolicyTests(CourseDetailTestMixin, CourseViewTestsMixin, Shar
self
.
assertListEqual
(
response
.
data
,
expected
)
self
.
assertListEqual
(
response
.
data
,
expected
)
class
CourseGradingPolicyMissingFieldsTests
(
CourseDetailTestMixin
,
CourseViewTestsMixin
,
SharedModuleStoreTestCase
):
view
=
'course_structure_api:v0:grading_policy'
# Update the raw grader to have missing keys
raw_grader
=
[
{
"min_count"
:
24
,
"weight"
:
0.2
,
"type"
:
"Homework"
,
"drop_count"
:
0
,
"short_label"
:
"HW"
},
{
# Deleted "min_count" key
"weight"
:
0.8
,
"type"
:
"Exam"
,
"drop_count"
:
0
,
"short_label"
:
"Exam"
}
]
@classmethod
def
setUpClass
(
cls
):
super
(
CourseGradingPolicyMissingFieldsTests
,
cls
)
.
setUpClass
()
cls
.
create_course_data
()
def
test_get
(
self
):
"""
The view should return grading policy for a course.
"""
response
=
super
(
CourseGradingPolicyMissingFieldsTests
,
self
)
.
test_get
()
expected
=
[
{
"count"
:
24
,
"weight"
:
0.2
,
"assignment_type"
:
"Homework"
,
"dropped"
:
0
},
{
"count"
:
None
,
"weight"
:
0.8
,
"assignment_type"
:
"Exam"
,
"dropped"
:
0
}
]
self
.
assertListEqual
(
response
.
data
,
expected
)
#####################################################################################
#####################################################################################
#
#
# The following Mixins/Classes collectively test the CourseBlocksAndNavigation view.
# The following Mixins/Classes collectively test the CourseBlocksAndNavigation view.
...
...
lms/djangoapps/course_structure_api/v0/views.py
View file @
2fd6add5
...
@@ -6,8 +6,7 @@ import logging
...
@@ -6,8 +6,7 @@ import logging
from
django.conf
import
settings
from
django.conf
import
settings
from
django.http
import
Http404
from
django.http
import
Http404
from
rest_framework.authentication
import
SessionAuthentication
from
rest_framework.authentication
import
OAuth2Authentication
,
SessionAuthentication
from
rest_framework_oauth.authentication
import
OAuth2Authentication
from
rest_framework.exceptions
import
AuthenticationFailed
,
ParseError
from
rest_framework.exceptions
import
AuthenticationFailed
,
ParseError
from
rest_framework.generics
import
RetrieveAPIView
,
ListAPIView
from
rest_framework.generics
import
RetrieveAPIView
,
ListAPIView
from
rest_framework.permissions
import
IsAuthenticated
from
rest_framework.permissions
import
IsAuthenticated
...
@@ -22,6 +21,7 @@ from courseware.access import has_access
...
@@ -22,6 +21,7 @@ from courseware.access import has_access
from
courseware.model_data
import
FieldDataCache
from
courseware.model_data
import
FieldDataCache
from
courseware.module_render
import
get_module_for_descriptor
from
courseware.module_render
import
get_module_for_descriptor
from
openedx.core.lib.api.view_utils
import
view_course_access
,
view_auth_classes
from
openedx.core.lib.api.view_utils
import
view_course_access
,
view_auth_classes
from
openedx.core.lib.api.serializers
import
PaginationSerializer
from
openedx.core.djangoapps.content.course_structures.api.v0
import
api
,
errors
from
openedx.core.djangoapps.content.course_structures.api.v0
import
api
,
errors
from
student.roles
import
CourseInstructorRole
,
CourseStaffRole
from
student.roles
import
CourseInstructorRole
,
CourseStaffRole
from
util.module_utils
import
get_dynamic_descriptor_children
from
util.module_utils
import
get_dynamic_descriptor_children
...
@@ -157,6 +157,9 @@ class CourseList(CourseViewMixin, ListAPIView):
...
@@ -157,6 +157,9 @@ class CourseList(CourseViewMixin, ListAPIView):
* end: The course end date. If course end date is not specified, the
* end: The course end date. If course end date is not specified, the
value is null.
value is null.
"""
"""
paginate_by
=
10
paginate_by_param
=
'page_size'
pagination_serializer_class
=
PaginationSerializer
serializer_class
=
serializers
.
CourseSerializer
serializer_class
=
serializers
.
CourseSerializer
def
get_queryset
(
self
):
def
get_queryset
(
self
):
...
...
lms/djangoapps/courseware/grades.py
View file @
2fd6add5
...
@@ -25,6 +25,7 @@ from xmodule.modulestore.django import modulestore
...
@@ -25,6 +25,7 @@ from xmodule.modulestore.django import modulestore
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
.models
import
StudentModule
from
.models
import
StudentModule
from
.module_render
import
get_module_for_descriptor
from
.module_render
import
get_module_for_descriptor
from
submissions
import
api
as
sub_api
# installed from the edx-submissions repository
from
opaque_keys
import
InvalidKeyError
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
openedx.core.djangoapps.signals.signals
import
GRADES_UPDATED
from
openedx.core.djangoapps.signals.signals
import
GRADES_UPDATED
...
@@ -348,13 +349,8 @@ def _grade(student, request, course, keep_raw_scores, field_data_cache, scores_c
...
@@ -348,13 +349,8 @@ def _grade(student, request, course, keep_raw_scores, field_data_cache, scores_c
# Dict of item_ids -> (earned, possible) point tuples. This *only* grabs
# Dict of item_ids -> (earned, possible) point tuples. This *only* grabs
# scores that were registered with the submissions API, which for the moment
# scores that were registered with the submissions API, which for the moment
# means only openassessment (edx-ora2)
# means only openassessment (edx-ora2)
# We need to import this here to avoid a circular dependency of the form:
# XBlock --> submissions --> Django Rest Framework error strings -->
# Django translation --> ... --> courseware --> submissions
from
submissions
import
api
as
sub_api
# installed from the edx-submissions repository
submissions_scores
=
sub_api
.
get_scores
(
course
.
id
.
to_deprecated_string
(),
anonymous_id_for_user
(
student
,
course
.
id
))
submissions_scores
=
sub_api
.
get_scores
(
course
.
id
.
to_deprecated_string
(),
anonymous_id_for_user
(
student
,
course
.
id
))
max_scores_cache
=
MaxScoresCache
.
create_for_course
(
course
)
max_scores_cache
=
MaxScoresCache
.
create_for_course
(
course
)
# For the moment, we have to get scorable_locations from field_data_cache
# For the moment, we have to get scorable_locations from field_data_cache
# and not from scores_client, because scores_client is ignorant of things
# and not from scores_client, because scores_client is ignorant of things
# in the submissions API. As a further refactoring step, submissions should
# in the submissions API. As a further refactoring step, submissions should
...
@@ -569,12 +565,7 @@ def _progress_summary(student, request, course, field_data_cache=None, scores_cl
...
@@ -569,12 +565,7 @@ def _progress_summary(student, request, course, field_data_cache=None, scores_cl
course_module
=
getattr
(
course_module
,
'_x_module'
,
course_module
)
course_module
=
getattr
(
course_module
,
'_x_module'
,
course_module
)
# We need to import this here to avoid a circular dependency of the form:
# XBlock --> submissions --> Django Rest Framework error strings -->
# Django translation --> ... --> courseware --> submissions
from
submissions
import
api
as
sub_api
# installed from the edx-submissions repository
submissions_scores
=
sub_api
.
get_scores
(
course
.
id
.
to_deprecated_string
(),
anonymous_id_for_user
(
student
,
course
.
id
))
submissions_scores
=
sub_api
.
get_scores
(
course
.
id
.
to_deprecated_string
(),
anonymous_id_for_user
(
student
,
course
.
id
))
max_scores_cache
=
MaxScoresCache
.
create_for_course
(
course
)
max_scores_cache
=
MaxScoresCache
.
create_for_course
(
course
)
# For the moment, we have to get scorable_locations from field_data_cache
# For the moment, we have to get scorable_locations from field_data_cache
# and not from scores_client, because scores_client is ignorant of things
# and not from scores_client, because scores_client is ignorant of things
...
...
lms/djangoapps/discussion_api/api.py
View file @
2fd6add5
...
@@ -560,7 +560,7 @@ def create_thread(request, thread_data):
...
@@ -560,7 +560,7 @@ def create_thread(request, thread_data):
if
not
(
serializer
.
is_valid
()
and
actions_form
.
is_valid
()):
if
not
(
serializer
.
is_valid
()
and
actions_form
.
is_valid
()):
raise
ValidationError
(
dict
(
serializer
.
errors
.
items
()
+
actions_form
.
errors
.
items
()))
raise
ValidationError
(
dict
(
serializer
.
errors
.
items
()
+
actions_form
.
errors
.
items
()))
serializer
.
save
()
serializer
.
save
()
cc_thread
=
serializer
.
instance
cc_thread
=
serializer
.
object
thread_created
.
send
(
sender
=
None
,
user
=
user
,
post
=
cc_thread
)
thread_created
.
send
(
sender
=
None
,
user
=
user
,
post
=
cc_thread
)
api_thread
=
serializer
.
data
api_thread
=
serializer
.
data
_do_extra_actions
(
api_thread
,
cc_thread
,
thread_data
.
keys
(),
actions_form
,
context
)
_do_extra_actions
(
api_thread
,
cc_thread
,
thread_data
.
keys
(),
actions_form
,
context
)
...
@@ -606,7 +606,7 @@ def create_comment(request, comment_data):
...
@@ -606,7 +606,7 @@ def create_comment(request, comment_data):
if
not
(
serializer
.
is_valid
()
and
actions_form
.
is_valid
()):
if
not
(
serializer
.
is_valid
()
and
actions_form
.
is_valid
()):
raise
ValidationError
(
dict
(
serializer
.
errors
.
items
()
+
actions_form
.
errors
.
items
()))
raise
ValidationError
(
dict
(
serializer
.
errors
.
items
()
+
actions_form
.
errors
.
items
()))
serializer
.
save
()
serializer
.
save
()
cc_comment
=
serializer
.
instance
cc_comment
=
serializer
.
object
comment_created
.
send
(
sender
=
None
,
user
=
request
.
user
,
post
=
cc_comment
)
comment_created
.
send
(
sender
=
None
,
user
=
request
.
user
,
post
=
cc_comment
)
api_comment
=
serializer
.
data
api_comment
=
serializer
.
data
_do_extra_actions
(
api_comment
,
cc_comment
,
comment_data
.
keys
(),
actions_form
,
context
)
_do_extra_actions
(
api_comment
,
cc_comment
,
comment_data
.
keys
(),
actions_form
,
context
)
...
...
lms/djangoapps/discussion_api/pagination.py
View file @
2fd6add5
"""
"""
Discussion API pagination support
Discussion API pagination support
"""
"""
from
rest_framework.utils.urls
import
replace_query_param
from
rest_framework.pagination
import
BasePaginationSerializer
,
NextPageField
,
PreviousPageField
class
_PaginationSerializer
(
BasePaginationSerializer
):
"""
A pagination serializer without the count field, because the Comments
Service does not return result counts
"""
next
=
NextPageField
(
source
=
"*"
)
previous
=
PreviousPageField
(
source
=
"*"
)
class
_Page
(
object
):
class
_Page
(
object
):
...
@@ -43,25 +52,7 @@ def get_paginated_data(request, results, page_num, per_page):
...
@@ -43,25 +52,7 @@ def get_paginated_data(request, results, page_num, per_page):
previous: The URL for the previous page
previous: The URL for the previous page
results: The results on this page
results: The results on this page
"""
"""
# Note: Previous versions of this function used Django Rest Framework's
return
_PaginationSerializer
(
# paginated serializer. With the upgrade to DRF 3.1, paginated serializers
instance
=
_Page
(
results
,
page_num
,
per_page
),
# have been removed. We *could* use DRF's paginator classes, but there are
context
=
{
"request"
:
request
}
# some slight differences between how DRF does pagination and how we're doing
)
.
data
# pagination here. (For example, we respond with a next_url param even if
# there is only one result on the current page.) To maintain backwards
# compatability, we simulate the behavior that DRF used to provide.
page
=
_Page
(
results
,
page_num
,
per_page
)
next_url
,
previous_url
=
None
,
None
base_url
=
request
.
build_absolute_uri
()
if
page
.
has_next
():
next_url
=
replace_query_param
(
base_url
,
"page"
,
page
.
next_page_number
())
if
page
.
has_previous
():
previous_url
=
replace_query_param
(
base_url
,
"page"
,
page
.
previous_page_number
())
return
{
"next"
:
next_url
,
"previous"
:
previous_url
,
"results"
:
results
,
}
lms/djangoapps/discussion_api/serializers.py
View file @
2fd6add5
...
@@ -28,6 +28,7 @@ from lms.lib.comment_client.thread import Thread
...
@@ -28,6 +28,7 @@ from lms.lib.comment_client.thread import Thread
from
lms.lib.comment_client.user
import
User
as
CommentClientUser
from
lms.lib.comment_client.user
import
User
as
CommentClientUser
from
lms.lib.comment_client.utils
import
CommentClientRequestError
from
lms.lib.comment_client.utils
import
CommentClientRequestError
from
openedx.core.djangoapps.course_groups.cohorts
import
get_cohort_names
from
openedx.core.djangoapps.course_groups.cohorts
import
get_cohort_names
from
openedx.core.lib.api.fields
import
NonEmptyCharField
def
get_context
(
course
,
request
,
thread
=
None
):
def
get_context
(
course
,
request
,
thread
=
None
):
...
@@ -65,43 +66,36 @@ def get_context(course, request, thread=None):
...
@@ -65,43 +66,36 @@ def get_context(course, request, thread=None):
}
}
def
validate_not_blank
(
value
):
"""
Validate that a value is not an empty string or whitespace.
Raises: ValidationError
"""
if
not
value
.
strip
():
raise
ValidationError
(
"This field may not be blank."
)
class
_ContentSerializer
(
serializers
.
Serializer
):
class
_ContentSerializer
(
serializers
.
Serializer
):
"""A base class for thread and comment serializers."""
"""A base class for thread and comment serializers."""
id
=
serializers
.
CharField
(
read_only
=
True
)
# pylint: disable=invalid-name
id
_
=
serializers
.
CharField
(
read_only
=
True
)
author
=
serializers
.
SerializerMethodField
()
author
=
serializers
.
SerializerMethodField
(
"get_author"
)
author_label
=
serializers
.
SerializerMethodField
()
author_label
=
serializers
.
SerializerMethodField
(
"get_author_label"
)
created_at
=
serializers
.
CharField
(
read_only
=
True
)
created_at
=
serializers
.
CharField
(
read_only
=
True
)
updated_at
=
serializers
.
CharField
(
read_only
=
True
)
updated_at
=
serializers
.
CharField
(
read_only
=
True
)
raw_body
=
serializers
.
CharField
(
source
=
"body"
,
validators
=
[
validate_not_blank
]
)
raw_body
=
NonEmptyCharField
(
source
=
"body"
)
rendered_body
=
serializers
.
SerializerMethodField
()
rendered_body
=
serializers
.
SerializerMethodField
(
"get_rendered_body"
)
abuse_flagged
=
serializers
.
SerializerMethodField
()
abuse_flagged
=
serializers
.
SerializerMethodField
(
"get_abuse_flagged"
)
voted
=
serializers
.
SerializerMethodField
()
voted
=
serializers
.
SerializerMethodField
(
"get_voted"
)
vote_count
=
serializers
.
SerializerMethodField
()
vote_count
=
serializers
.
SerializerMethodField
(
"get_vote_count"
)
editable_fields
=
serializers
.
SerializerMethodField
()
editable_fields
=
serializers
.
SerializerMethodField
(
"get_editable_fields"
)
non_updatable_fields
=
set
()
non_updatable_fields
=
set
()
def
__init__
(
self
,
*
args
,
**
kwargs
):
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
_ContentSerializer
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
super
(
_ContentSerializer
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
# id is an invalid class attribute name, so we must declare a different
# name above and modify it here
self
.
fields
[
"id"
]
=
self
.
fields
.
pop
(
"id_"
)
for
field
in
self
.
non_updatable_fields
:
for
field
in
self
.
non_updatable_fields
:
setattr
(
self
,
"validate_{}"
.
format
(
field
),
self
.
_validate_non_updatable
)
setattr
(
self
,
"validate_{}"
.
format
(
field
),
self
.
_validate_non_updatable
)
def
_validate_non_updatable
(
self
,
valu
e
):
def
_validate_non_updatable
(
self
,
attrs
,
_sourc
e
):
"""Ensure that a field is not edited in an update operation."""
"""Ensure that a field is not edited in an update operation."""
if
self
.
instance
:
if
self
.
object
:
raise
ValidationError
(
"This field is not allowed in an update."
)
raise
ValidationError
(
"This field is not allowed in an update."
)
return
value
return
attrs
def
_is_user_privileged
(
self
,
user_id
):
def
_is_user_privileged
(
self
,
user_id
):
"""
"""
...
@@ -137,11 +131,7 @@ class _ContentSerializer(serializers.Serializer):
...
@@ -137,11 +131,7 @@ class _ContentSerializer(serializers.Serializer):
def
get_author_label
(
self
,
obj
):
def
get_author_label
(
self
,
obj
):
"""Returns the role label for the content author."""
"""Returns the role label for the content author."""
if
self
.
_is_anonymous
(
obj
)
or
obj
[
"user_id"
]
is
None
:
return
None
if
self
.
_is_anonymous
(
obj
)
else
self
.
_get_user_label
(
int
(
obj
[
"user_id"
]))
return
None
else
:
user_id
=
int
(
obj
[
"user_id"
])
return
self
.
_get_user_label
(
user_id
)
def
get_rendered_body
(
self
,
obj
):
def
get_rendered_body
(
self
,
obj
):
"""Returns the rendered body content."""
"""Returns the rendered body content."""
...
@@ -152,7 +142,7 @@ class _ContentSerializer(serializers.Serializer):
...
@@ -152,7 +142,7 @@ class _ContentSerializer(serializers.Serializer):
Returns a boolean indicating whether the requester has flagged the
Returns a boolean indicating whether the requester has flagged the
content as abusive.
content as abusive.
"""
"""
return
self
.
context
[
"cc_requester"
][
"id"
]
in
obj
.
get
(
"abuse_flaggers"
,
[])
return
self
.
context
[
"cc_requester"
][
"id"
]
in
obj
[
"abuse_flaggers"
]
def
get_voted
(
self
,
obj
):
def
get_voted
(
self
,
obj
):
"""
"""
...
@@ -163,7 +153,7 @@ class _ContentSerializer(serializers.Serializer):
...
@@ -163,7 +153,7 @@ class _ContentSerializer(serializers.Serializer):
def
get_vote_count
(
self
,
obj
):
def
get_vote_count
(
self
,
obj
):
"""Returns the number of votes for the content."""
"""Returns the number of votes for the content."""
return
obj
.
get
(
"votes"
,
{})
.
get
(
"up_count"
,
0
)
return
obj
[
"votes"
][
"up_count"
]
def
get_editable_fields
(
self
,
obj
):
def
get_editable_fields
(
self
,
obj
):
"""Return the list of the fields the requester can edit"""
"""Return the list of the fields the requester can edit"""
...
@@ -179,28 +169,28 @@ class ThreadSerializer(_ContentSerializer):
...
@@ -179,28 +169,28 @@ class ThreadSerializer(_ContentSerializer):
at introspection and Thread's __getattr__.
at introspection and Thread's __getattr__.
"""
"""
course_id
=
serializers
.
CharField
()
course_id
=
serializers
.
CharField
()
topic_id
=
serializers
.
CharField
(
source
=
"commentable_id"
,
validators
=
[
validate_not_blank
]
)
topic_id
=
NonEmptyCharField
(
source
=
"commentable_id"
)
group_id
=
serializers
.
IntegerField
(
required
=
False
,
allow_null
=
True
)
group_id
=
serializers
.
IntegerField
(
required
=
False
)
group_name
=
serializers
.
SerializerMethodField
()
group_name
=
serializers
.
SerializerMethodField
(
"get_group_name"
)
type
=
serializers
.
ChoiceField
(
type
_
=
serializers
.
ChoiceField
(
source
=
"thread_type"
,
source
=
"thread_type"
,
choices
=
[(
val
,
val
)
for
val
in
[
"discussion"
,
"question"
]]
choices
=
[(
val
,
val
)
for
val
in
[
"discussion"
,
"question"
]]
)
)
title
=
serializers
.
CharField
(
validators
=
[
validate_not_blank
]
)
title
=
NonEmptyCharField
(
)
pinned
=
serializers
.
SerializerMethod
Field
(
read_only
=
True
)
pinned
=
serializers
.
Boolean
Field
(
read_only
=
True
)
closed
=
serializers
.
BooleanField
(
read_only
=
True
)
closed
=
serializers
.
BooleanField
(
read_only
=
True
)
following
=
serializers
.
SerializerMethodField
()
following
=
serializers
.
SerializerMethodField
(
"get_following"
)
comment_count
=
serializers
.
IntegerField
(
source
=
"comments_count"
,
read_only
=
True
)
comment_count
=
serializers
.
IntegerField
(
source
=
"comments_count"
,
read_only
=
True
)
unread_comment_count
=
serializers
.
IntegerField
(
source
=
"unread_comments_count"
,
read_only
=
True
)
unread_comment_count
=
serializers
.
IntegerField
(
source
=
"unread_comments_count"
,
read_only
=
True
)
comment_list_url
=
serializers
.
SerializerMethodField
()
comment_list_url
=
serializers
.
SerializerMethodField
(
"get_comment_list_url"
)
endorsed_comment_list_url
=
serializers
.
SerializerMethodField
()
endorsed_comment_list_url
=
serializers
.
SerializerMethodField
(
"get_endorsed_comment_list_url"
)
non_endorsed_comment_list_url
=
serializers
.
SerializerMethodField
()
non_endorsed_comment_list_url
=
serializers
.
SerializerMethodField
(
"get_non_endorsed_comment_list_url"
)
read
=
serializers
.
BooleanField
(
read_only
=
True
)
read
=
serializers
.
BooleanField
(
read_only
=
True
)
has_endorsed
=
serializers
.
BooleanField
(
read_only
=
True
,
source
=
"endorsed"
)
has_endorsed
=
serializers
.
BooleanField
(
read_only
=
True
,
source
=
"endorsed"
)
response_count
=
serializers
.
IntegerField
(
source
=
"resp_total"
,
read_only
=
True
)
response_count
=
serializers
.
IntegerField
(
source
=
"resp_total"
,
read_only
=
True
)
non_updatable_fields
=
NON_UPDATABLE_THREAD_FIELDS
non_updatable_fields
=
NON_UPDATABLE_THREAD_FIELDS
# TODO: https://openedx.atlassian.net/browse/MA-1359
# TODO: https://openedx.atlassian.net/browse/MA-1359
def
__init__
(
self
,
*
args
,
**
kwargs
):
def
__init__
(
self
,
*
args
,
**
kwargs
):
remove_fields
=
kwargs
.
pop
(
'remove_fields'
,
None
)
remove_fields
=
kwargs
.
pop
(
'remove_fields'
,
None
)
...
@@ -212,13 +202,6 @@ class ThreadSerializer(_ContentSerializer):
...
@@ -212,13 +202,6 @@ class ThreadSerializer(_ContentSerializer):
# not have the pinned field set
# not have the pinned field set
if
self
.
object
and
self
.
object
.
get
(
"pinned"
)
is
None
:
if
self
.
object
and
self
.
object
.
get
(
"pinned"
)
is
None
:
self
.
object
[
"pinned"
]
=
False
self
.
object
[
"pinned"
]
=
False
def
get_pinned
(
self
,
obj
):
"""
Compensate for the fact that some threads in the comments service do
not have the pinned field set.
"""
return
bool
(
obj
[
"pinned"
])
if
remove_fields
:
if
remove_fields
:
# for multiple fields in a list
# for multiple fields in a list
...
@@ -262,16 +245,13 @@ class ThreadSerializer(_ContentSerializer):
...
@@ -262,16 +245,13 @@ class ThreadSerializer(_ContentSerializer):
"""Returns the URL to retrieve the thread's non-endorsed comments."""
"""Returns the URL to retrieve the thread's non-endorsed comments."""
return
self
.
get_comment_list_url
(
obj
,
endorsed
=
False
)
return
self
.
get_comment_list_url
(
obj
,
endorsed
=
False
)
def
create
(
self
,
validated_data
):
def
restore_object
(
self
,
attrs
,
instance
=
None
):
thread
=
Thread
(
user_id
=
self
.
context
[
"cc_requester"
][
"id"
],
**
validated_data
)
if
instance
:
thread
.
save
()
for
key
,
val
in
attrs
.
items
():
return
thread
instance
[
key
]
=
val
return
instance
def
update
(
self
,
instance
,
validated_data
):
else
:
for
key
,
val
in
validated_data
.
items
():
return
Thread
(
user_id
=
self
.
context
[
"cc_requester"
][
"id"
],
**
attrs
)
instance
[
key
]
=
val
instance
.
save
()
return
instance
class
CommentSerializer
(
_ContentSerializer
):
class
CommentSerializer
(
_ContentSerializer
):
...
@@ -283,12 +263,12 @@ class CommentSerializer(_ContentSerializer):
...
@@ -283,12 +263,12 @@ class CommentSerializer(_ContentSerializer):
at introspection and Comment's __getattr__.
at introspection and Comment's __getattr__.
"""
"""
thread_id
=
serializers
.
CharField
()
thread_id
=
serializers
.
CharField
()
parent_id
=
serializers
.
CharField
(
required
=
False
,
allow_null
=
True
)
parent_id
=
serializers
.
CharField
(
required
=
False
)
endorsed
=
serializers
.
BooleanField
(
required
=
False
)
endorsed
=
serializers
.
BooleanField
(
required
=
False
)
endorsed_by
=
serializers
.
SerializerMethodField
()
endorsed_by
=
serializers
.
SerializerMethodField
(
"get_endorsed_by"
)
endorsed_by_label
=
serializers
.
SerializerMethodField
()
endorsed_by_label
=
serializers
.
SerializerMethodField
(
"get_endorsed_by_label"
)
endorsed_at
=
serializers
.
SerializerMethodField
()
endorsed_at
=
serializers
.
SerializerMethodField
(
"get_endorsed_at"
)
children
=
serializers
.
SerializerMethodField
()
children
=
serializers
.
SerializerMethodField
(
"get_children"
)
non_updatable_fields
=
NON_UPDATABLE_COMMENT_FIELDS
non_updatable_fields
=
NON_UPDATABLE_COMMENT_FIELDS
...
@@ -331,17 +311,6 @@ class CommentSerializer(_ContentSerializer):
...
@@ -331,17 +311,6 @@ class CommentSerializer(_ContentSerializer):
for
child
in
obj
.
get
(
"children"
,
[])
for
child
in
obj
.
get
(
"children"
,
[])
]
]
def
to_representation
(
self
,
data
):
data
=
super
(
CommentSerializer
,
self
)
.
to_representation
(
data
)
# Django Rest Framework v3 no longer includes None values
# in the representation. To maintain the previous behavior,
# we do this manually instead.
if
'parent_id'
not
in
data
:
data
[
"parent_id"
]
=
None
return
data
def
validate
(
self
,
attrs
):
def
validate
(
self
,
attrs
):
"""
"""
Ensure that parent_id identifies a comment that is actually in the
Ensure that parent_id identifies a comment that is actually in the
...
@@ -363,23 +332,18 @@ class CommentSerializer(_ContentSerializer):
...
@@ -363,23 +332,18 @@ class CommentSerializer(_ContentSerializer):
raise
ValidationError
({
"parent_id"
:
[
"Comment level is too deep."
]})
raise
ValidationError
({
"parent_id"
:
[
"Comment level is too deep."
]})
return
attrs
return
attrs
def
create
(
self
,
validated_data
):
def
restore_object
(
self
,
attrs
,
instance
=
None
):
comment
=
Comment
(
if
instance
:
for
key
,
val
in
attrs
.
items
():
instance
[
key
]
=
val
# TODO: The comments service doesn't populate the endorsement
# field on comment creation, so we only provide
# endorsement_user_id on update
if
key
==
"endorsed"
:
instance
[
"endorsement_user_id"
]
=
self
.
context
[
"cc_requester"
][
"id"
]
return
instance
return
Comment
(
course_id
=
self
.
context
[
"thread"
][
"course_id"
],
course_id
=
self
.
context
[
"thread"
][
"course_id"
],
user_id
=
self
.
context
[
"cc_requester"
][
"id"
],
user_id
=
self
.
context
[
"cc_requester"
][
"id"
],
**
validated_data
**
attrs
)
)
comment
.
save
()
return
comment
def
update
(
self
,
instance
,
validated_data
):
for
key
,
val
in
validated_data
.
items
():
instance
[
key
]
=
val
# TODO: The comments service doesn't populate the endorsement
# field on comment creation, so we only provide
# endorsement_user_id on update
if
key
==
"endorsed"
:
instance
[
"endorsement_user_id"
]
=
self
.
context
[
"cc_requester"
][
"id"
]
instance
.
save
()
return
instance
lms/djangoapps/discussion_api/tests/test_api.py
View file @
2fd6add5
...
@@ -1497,9 +1497,8 @@ class CreateThreadTest(
...
@@ -1497,9 +1497,8 @@ class CreateThreadTest(
self
.
assertEqual
(
actual_post_data
[
"group_id"
],
[
str
(
cohort
.
id
)])
self
.
assertEqual
(
actual_post_data
[
"group_id"
],
[
str
(
cohort
.
id
)])
else
:
else
:
self
.
assertNotIn
(
"group_id"
,
actual_post_data
)
self
.
assertNotIn
(
"group_id"
,
actual_post_data
)
except
ValidationError
as
ex
:
except
ValidationError
:
if
not
expected_error
:
self
.
assertTrue
(
expected_error
)
self
.
fail
(
"Unexpected validation error: {}"
.
format
(
ex
))
def
test_following
(
self
):
def
test_following
(
self
):
self
.
register_post_thread_response
({
"id"
:
"test_id"
})
self
.
register_post_thread_response
({
"id"
:
"test_id"
})
...
@@ -2240,7 +2239,7 @@ class UpdateThreadTest(
...
@@ -2240,7 +2239,7 @@ class UpdateThreadTest(
update_thread
(
self
.
request
,
"test_thread"
,
{
"raw_body"
:
""
})
update_thread
(
self
.
request
,
"test_thread"
,
{
"raw_body"
:
""
})
self
.
assertEqual
(
self
.
assertEqual
(
assertion
.
exception
.
message_dict
,
assertion
.
exception
.
message_dict
,
{
"raw_body"
:
[
"This field
may not be blank
."
]}
{
"raw_body"
:
[
"This field
is required
."
]}
)
)
...
...
lms/djangoapps/discussion_api/tests/test_serializers.py
View file @
2fd6add5
...
@@ -523,10 +523,9 @@ class ThreadSerializerDeserializationTest(CommentsServiceMockMixin, UrlResetMixi
...
@@ -523,10 +523,9 @@ class ThreadSerializerDeserializationTest(CommentsServiceMockMixin, UrlResetMixi
data
=
self
.
minimal_data
.
copy
()
data
=
self
.
minimal_data
.
copy
()
data
.
update
({
field
:
value
for
field
in
[
"topic_id"
,
"title"
,
"raw_body"
]})
data
.
update
({
field
:
value
for
field
in
[
"topic_id"
,
"title"
,
"raw_body"
]})
serializer
=
ThreadSerializer
(
data
=
data
,
context
=
get_context
(
self
.
course
,
self
.
request
))
serializer
=
ThreadSerializer
(
data
=
data
,
context
=
get_context
(
self
.
course
,
self
.
request
))
self
.
assertFalse
(
serializer
.
is_valid
())
self
.
assertEqual
(
self
.
assertEqual
(
serializer
.
errors
,
serializer
.
errors
,
{
field
:
[
"This field
may not be blank
."
]
for
field
in
[
"topic_id"
,
"title"
,
"raw_body"
]}
{
field
:
[
"This field
is required
."
]
for
field
in
[
"topic_id"
,
"title"
,
"raw_body"
]}
)
)
def
test_create_type
(
self
):
def
test_create_type
(
self
):
...
@@ -593,10 +592,9 @@ class ThreadSerializerDeserializationTest(CommentsServiceMockMixin, UrlResetMixi
...
@@ -593,10 +592,9 @@ class ThreadSerializerDeserializationTest(CommentsServiceMockMixin, UrlResetMixi
partial
=
True
,
partial
=
True
,
context
=
get_context
(
self
.
course
,
self
.
request
)
context
=
get_context
(
self
.
course
,
self
.
request
)
)
)
self
.
assertFalse
(
serializer
.
is_valid
())
self
.
assertEqual
(
self
.
assertEqual
(
serializer
.
errors
,
serializer
.
errors
,
{
field
:
[
"This field
may not be blank
."
]
for
field
in
[
"topic_id"
,
"title"
,
"raw_body"
]}
{
field
:
[
"This field
is required
."
]
for
field
in
[
"topic_id"
,
"title"
,
"raw_body"
]}
)
)
def
test_update_course_id
(
self
):
def
test_update_course_id
(
self
):
...
@@ -606,7 +604,6 @@ class ThreadSerializerDeserializationTest(CommentsServiceMockMixin, UrlResetMixi
...
@@ -606,7 +604,6 @@ class ThreadSerializerDeserializationTest(CommentsServiceMockMixin, UrlResetMixi
partial
=
True
,
partial
=
True
,
context
=
get_context
(
self
.
course
,
self
.
request
)
context
=
get_context
(
self
.
course
,
self
.
request
)
)
)
self
.
assertFalse
(
serializer
.
is_valid
())
self
.
assertEqual
(
self
.
assertEqual
(
serializer
.
errors
,
serializer
.
errors
,
{
"course_id"
:
[
"This field is not allowed in an update."
]}
{
"course_id"
:
[
"This field is not allowed in an update."
]}
...
@@ -772,7 +769,7 @@ class CommentSerializerDeserializationTest(CommentsServiceMockMixin, SharedModul
...
@@ -772,7 +769,7 @@ class CommentSerializerDeserializationTest(CommentsServiceMockMixin, SharedModul
data
[
"parent_id"
]
=
None
data
[
"parent_id"
]
=
None
serializer
=
CommentSerializer
(
data
=
data
,
context
=
context
)
serializer
=
CommentSerializer
(
data
=
data
,
context
=
context
)
self
.
assertFalse
(
serializer
.
is_valid
())
self
.
assertFalse
(
serializer
.
is_valid
())
self
.
assertEqual
(
serializer
.
errors
,
{
"
non_field_errors
"
:
[
"Comment level is too deep."
]})
self
.
assertEqual
(
serializer
.
errors
,
{
"
parent_id
"
:
[
"Comment level is too deep."
]})
def
test_create_missing_field
(
self
):
def
test_create_missing_field
(
self
):
for
field
in
self
.
minimal_data
:
for
field
in
self
.
minimal_data
:
...
@@ -858,10 +855,9 @@ class CommentSerializerDeserializationTest(CommentsServiceMockMixin, SharedModul
...
@@ -858,10 +855,9 @@ class CommentSerializerDeserializationTest(CommentsServiceMockMixin, SharedModul
partial
=
True
,
partial
=
True
,
context
=
get_context
(
self
.
course
,
self
.
request
)
context
=
get_context
(
self
.
course
,
self
.
request
)
)
)
self
.
assertFalse
(
serializer
.
is_valid
())
self
.
assertEqual
(
self
.
assertEqual
(
serializer
.
errors
,
serializer
.
errors
,
{
"raw_body"
:
[
"This field
may not be blank
."
]}
{
"raw_body"
:
[
"This field
is required
."
]}
)
)
@ddt.data
(
"thread_id"
,
"parent_id"
)
@ddt.data
(
"thread_id"
,
"parent_id"
)
...
@@ -872,7 +868,6 @@ class CommentSerializerDeserializationTest(CommentsServiceMockMixin, SharedModul
...
@@ -872,7 +868,6 @@ class CommentSerializerDeserializationTest(CommentsServiceMockMixin, SharedModul
partial
=
True
,
partial
=
True
,
context
=
get_context
(
self
.
course
,
self
.
request
)
context
=
get_context
(
self
.
course
,
self
.
request
)
)
)
self
.
assertFalse
(
serializer
.
is_valid
())
self
.
assertEqual
(
self
.
assertEqual
(
serializer
.
errors
,
serializer
.
errors
,
{
field
:
[
"This field is not allowed in an update."
]}
{
field
:
[
"This field is not allowed in an update."
]}
...
...
lms/djangoapps/discussion_api/tests/test_views.py
View file @
2fd6add5
...
@@ -571,7 +571,7 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest
...
@@ -571,7 +571,7 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest
content_type
=
"application/json"
content_type
=
"application/json"
)
)
expected_response_data
=
{
expected_response_data
=
{
"field_errors"
:
{
"title"
:
{
"developer_message"
:
"This field
may not be blank
."
}}
"field_errors"
:
{
"title"
:
{
"developer_message"
:
"This field
is required
."
}}
}
}
self
.
assertEqual
(
response
.
status_code
,
400
)
self
.
assertEqual
(
response
.
status_code
,
400
)
response_data
=
json
.
loads
(
response
.
content
)
response_data
=
json
.
loads
(
response
.
content
)
...
@@ -690,7 +690,7 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
...
@@ -690,7 +690,7 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
200
,
200
,
{
{
"results"
:
expected_comments
,
"results"
:
expected_comments
,
"next"
:
"http://testserver/api/discussion/v1/comments/?
page=2&thread_id={}
"
.
format
(
"next"
:
"http://testserver/api/discussion/v1/comments/?
thread_id={}&page=2
"
.
format
(
self
.
thread_id
self
.
thread_id
),
),
"previous"
:
None
,
"previous"
:
None
,
...
@@ -946,7 +946,7 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes
...
@@ -946,7 +946,7 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes
content_type
=
"application/json"
content_type
=
"application/json"
)
)
expected_response_data
=
{
expected_response_data
=
{
"field_errors"
:
{
"raw_body"
:
{
"developer_message"
:
"This field
may not be blank
."
}}
"field_errors"
:
{
"raw_body"
:
{
"developer_message"
:
"This field
is required
."
}}
}
}
self
.
assertEqual
(
response
.
status_code
,
400
)
self
.
assertEqual
(
response
.
status_code
,
400
)
response_data
=
json
.
loads
(
response
.
content
)
response_data
=
json
.
loads
(
response
.
content
)
...
...
lms/djangoapps/discussion_api/views.py
View file @
2fd6add5
...
@@ -3,8 +3,7 @@ Discussion API views
...
@@ -3,8 +3,7 @@ Discussion API views
"""
"""
from
django.core.exceptions
import
ValidationError
from
django.core.exceptions
import
ValidationError
from
rest_framework.authentication
import
SessionAuthentication
from
rest_framework.authentication
import
OAuth2Authentication
,
SessionAuthentication
from
rest_framework_oauth.authentication
import
OAuth2Authentication
from
rest_framework.permissions
import
IsAuthenticated
from
rest_framework.permissions
import
IsAuthenticated
from
rest_framework.response
import
Response
from
rest_framework.response
import
Response
from
rest_framework.views
import
APIView
from
rest_framework.views
import
APIView
...
...
lms/djangoapps/django_comment_client/base/tests.py
View file @
2fd6add5
...
@@ -32,6 +32,8 @@ from xmodule.modulestore.tests.factories import check_mongo_calls
...
@@ -32,6 +32,8 @@ from xmodule.modulestore.tests.factories import check_mongo_calls
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore
import
ModuleStoreEnum
from
teams.tests.factories
import
CourseTeamFactory
log
=
logging
.
getLogger
(
__name__
)
log
=
logging
.
getLogger
(
__name__
)
...
@@ -1288,7 +1290,6 @@ class TeamsPermissionsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSe
...
@@ -1288,7 +1290,6 @@ class TeamsPermissionsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSe
topic_id
=
'topic_id'
,
topic_id
=
'topic_id'
,
discussion_topic_id
=
self
.
team_commentable_id
discussion_topic_id
=
self
.
team_commentable_id
)
)
self
.
team
.
add_user
(
self
.
student_in_team
)
self
.
team
.
add_user
(
self
.
student_in_team
)
# Dummy commentable ID not linked to a team
# Dummy commentable ID not linked to a team
...
...
lms/djangoapps/django_comment_client/forum/tests.py
View file @
2fd6add5
...
@@ -717,7 +717,6 @@ class InlineDiscussionContextTestCase(ModuleStoreTestCase):
...
@@ -717,7 +717,6 @@ class InlineDiscussionContextTestCase(ModuleStoreTestCase):
topic_id
=
'topic_id'
,
topic_id
=
'topic_id'
,
discussion_topic_id
=
self
.
discussion_topic_id
discussion_topic_id
=
self
.
discussion_topic_id
)
)
self
.
team
.
add_user
(
self
.
user
)
# pylint: disable=no-member
self
.
team
.
add_user
(
self
.
user
)
# pylint: disable=no-member
def
test_context_can_be_standalone
(
self
,
mock_request
):
def
test_context_can_be_standalone
(
self
,
mock_request
):
...
@@ -1094,9 +1093,7 @@ class InlineDiscussionTestCase(ModuleStoreTestCase):
...
@@ -1094,9 +1093,7 @@ class InlineDiscussionTestCase(ModuleStoreTestCase):
course_id
=
self
.
course
.
id
,
course_id
=
self
.
course
.
id
,
discussion_topic_id
=
self
.
discussion1
.
discussion_id
discussion_topic_id
=
self
.
discussion1
.
discussion_id
)
)
team
.
add_user
(
self
.
student
)
# pylint: disable=no-member
team
.
add_user
(
self
.
student
)
# pylint: disable=no-member
response
=
self
.
send_request
(
mock_request
)
response
=
self
.
send_request
(
mock_request
)
self
.
assertEqual
(
mock_request
.
call_args
[
1
][
'params'
][
'context'
],
ThreadContext
.
STANDALONE
)
self
.
assertEqual
(
mock_request
.
call_args
[
1
][
'params'
][
'context'
],
ThreadContext
.
STANDALONE
)
self
.
verify_response
(
response
)
self
.
verify_response
(
response
)
...
...
lms/djangoapps/django_comment_client/tests/group_id.py
View file @
2fd6add5
...
@@ -149,8 +149,6 @@ class NonCohortedTopicGroupIdTestMixin(GroupIdAssertionMixin):
...
@@ -149,8 +149,6 @@ class NonCohortedTopicGroupIdTestMixin(GroupIdAssertionMixin):
def
test_team_discussion_id_not_cohorted
(
self
,
mock_request
):
def
test_team_discussion_id_not_cohorted
(
self
,
mock_request
):
team
=
CourseTeamFactory
(
course_id
=
self
.
course
.
id
)
team
=
CourseTeamFactory
(
course_id
=
self
.
course
.
id
)
team
.
add_user
(
self
.
student
)
# pylint: disable=no-member
team
.
add_user
(
self
.
student
)
# pylint: disable=no-member
self
.
call_view
(
mock_request
,
team
.
discussion_topic_id
,
self
.
student
,
None
)
self
.
call_view
(
mock_request
,
team
.
discussion_topic_id
,
self
.
student
,
None
)
self
.
_assert_comments_service_called_without_group_id
(
mock_request
)
self
.
_assert_comments_service_called_without_group_id
(
mock_request
)
lms/djangoapps/mobile_api/social_facebook/courses/views.py
View file @
2fd6add5
...
@@ -31,7 +31,7 @@ class CoursesWithFriends(generics.ListAPIView):
...
@@ -31,7 +31,7 @@ class CoursesWithFriends(generics.ListAPIView):
serializer_class
=
serializers
.
CoursesWithFriendsSerializer
serializer_class
=
serializers
.
CoursesWithFriendsSerializer
def
list
(
self
,
request
,
*
args
,
**
kwargs
):
def
list
(
self
,
request
,
*
args
,
**
kwargs
):
serializer
=
self
.
get_serializer
(
data
=
request
.
GET
)
serializer
=
self
.
get_serializer
(
data
=
request
.
GET
,
files
=
request
.
FILES
)
if
not
serializer
.
is_valid
():
if
not
serializer
.
is_valid
():
return
Response
(
serializer
.
errors
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
return
Response
(
serializer
.
errors
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
...
@@ -61,5 +61,4 @@ class CoursesWithFriends(generics.ListAPIView):
...
@@ -61,5 +61,4 @@ class CoursesWithFriends(generics.ListAPIView):
and
is_mobile_available_for_user
(
self
.
request
.
user
,
enrollment
.
course
)
and
is_mobile_available_for_user
(
self
.
request
.
user
,
enrollment
.
course
)
]
]
serializer
=
CourseEnrollmentSerializer
(
courses
,
context
=
{
'request'
:
request
},
many
=
True
)
return
Response
(
CourseEnrollmentSerializer
(
courses
,
context
=
{
'request'
:
request
})
.
data
)
return
Response
(
serializer
.
data
)
lms/djangoapps/mobile_api/social_facebook/friends/views.py
View file @
2fd6add5
...
@@ -40,7 +40,7 @@ class FriendsInCourse(generics.ListAPIView):
...
@@ -40,7 +40,7 @@ class FriendsInCourse(generics.ListAPIView):
serializer_class
=
serializers
.
FriendsInCourseSerializer
serializer_class
=
serializers
.
FriendsInCourseSerializer
def
list
(
self
,
request
,
*
args
,
**
kwargs
):
def
list
(
self
,
request
,
*
args
,
**
kwargs
):
serializer
=
self
.
get_serializer
(
data
=
request
.
GET
)
serializer
=
self
.
get_serializer
(
data
=
request
.
GET
,
files
=
request
.
FILES
)
if
not
serializer
.
is_valid
():
if
not
serializer
.
is_valid
():
return
Response
(
serializer
.
errors
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
return
Response
(
serializer
.
errors
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
...
...
lms/djangoapps/mobile_api/social_facebook/groups/views.py
View file @
2fd6add5
...
@@ -45,7 +45,7 @@ class Groups(generics.CreateAPIView, mixins.DestroyModelMixin):
...
@@ -45,7 +45,7 @@ class Groups(generics.CreateAPIView, mixins.DestroyModelMixin):
serializer_class
=
serializers
.
GroupSerializer
serializer_class
=
serializers
.
GroupSerializer
def
create
(
self
,
request
,
*
args
,
**
kwargs
):
def
create
(
self
,
request
,
*
args
,
**
kwargs
):
serializer
=
self
.
get_serializer
(
data
=
request
.
DATA
)
serializer
=
self
.
get_serializer
(
data
=
request
.
DATA
,
files
=
request
.
FILES
)
if
not
serializer
.
is_valid
():
if
not
serializer
.
is_valid
():
return
Response
(
serializer
.
errors
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
return
Response
(
serializer
.
errors
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
try
:
try
:
...
@@ -106,12 +106,12 @@ class GroupsMembers(generics.CreateAPIView, mixins.DestroyModelMixin):
...
@@ -106,12 +106,12 @@ class GroupsMembers(generics.CreateAPIView, mixins.DestroyModelMixin):
serializer_class
=
serializers
.
GroupsMembersSerializer
serializer_class
=
serializers
.
GroupsMembersSerializer
def
create
(
self
,
request
,
*
args
,
**
kwargs
):
def
create
(
self
,
request
,
*
args
,
**
kwargs
):
serializer
=
self
.
get_serializer
(
data
=
request
.
DATA
)
serializer
=
self
.
get_serializer
(
data
=
request
.
DATA
,
files
=
request
.
FILES
)
if
not
serializer
.
is_valid
():
if
not
serializer
.
is_valid
():
return
Response
(
serializer
.
errors
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
return
Response
(
serializer
.
errors
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
graph
=
facebook_graph_api
()
graph
=
facebook_graph_api
()
url
=
settings
.
FACEBOOK_API_VERSION
+
'/'
+
kwargs
[
'group_id'
]
+
"/members"
url
=
settings
.
FACEBOOK_API_VERSION
+
'/'
+
kwargs
[
'group_id'
]
+
"/members"
member_ids
=
serializer
.
data
[
'member_ids'
]
.
split
(
','
)
member_ids
=
serializer
.
object
[
'member_ids'
]
.
split
(
','
)
response
=
{}
response
=
{}
for
member_id
in
member_ids
:
for
member_id
in
member_ids
:
try
:
try
:
...
...
lms/djangoapps/mobile_api/social_facebook/preferences/serializers.py
View file @
2fd6add5
...
@@ -8,4 +8,4 @@ class UserSharingSerializar(serializers.Serializer):
...
@@ -8,4 +8,4 @@ class UserSharingSerializar(serializers.Serializer):
"""
"""
Serializes user social settings
Serializes user social settings
"""
"""
share_with_facebook_friends
=
serializers
.
BooleanField
(
required
=
True
)
share_with_facebook_friends
=
serializers
.
BooleanField
(
required
=
True
,
default
=
False
)
lms/djangoapps/mobile_api/social_facebook/preferences/views.py
View file @
2fd6add5
...
@@ -39,9 +39,9 @@ class UserSharing(generics.ListCreateAPIView):
...
@@ -39,9 +39,9 @@ class UserSharing(generics.ListCreateAPIView):
serializer_class
=
serializers
.
UserSharingSerializar
serializer_class
=
serializers
.
UserSharingSerializar
def
create
(
self
,
request
,
*
args
,
**
kwargs
):
def
create
(
self
,
request
,
*
args
,
**
kwargs
):
serializer
=
self
.
get_serializer
(
data
=
request
.
DATA
)
serializer
=
self
.
get_serializer
(
data
=
request
.
DATA
,
files
=
request
.
FILES
)
if
serializer
.
is_valid
():
if
serializer
.
is_valid
():
value
=
serializer
.
data
[
'share_with_facebook_friends'
]
value
=
serializer
.
object
[
'share_with_facebook_friends'
]
set_user_preference
(
request
.
user
,
"share_with_facebook_friends"
,
value
)
set_user_preference
(
request
.
user
,
"share_with_facebook_friends"
,
value
)
return
self
.
get
(
request
,
*
args
,
**
kwargs
)
return
self
.
get
(
request
,
*
args
,
**
kwargs
)
return
Response
(
serializer
.
errors
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
return
Response
(
serializer
.
errors
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
...
...
lms/djangoapps/mobile_api/social_facebook/utils.py
View file @
2fd6add5
...
@@ -40,7 +40,7 @@ def get_friends_from_facebook(serializer):
...
@@ -40,7 +40,7 @@ def get_friends_from_facebook(serializer):
the error message.
the error message.
"""
"""
try
:
try
:
graph
=
facebook
.
GraphAPI
(
serializer
.
data
[
'oauth_token'
])
graph
=
facebook
.
GraphAPI
(
serializer
.
object
[
'oauth_token'
])
friends
=
graph
.
request
(
settings
.
FACEBOOK_API_VERSION
+
"/me/friends"
)
friends
=
graph
.
request
(
settings
.
FACEBOOK_API_VERSION
+
"/me/friends"
)
return
get_pagination
(
friends
)
return
get_pagination
(
friends
)
except
facebook
.
GraphAPIError
,
ex
:
except
facebook
.
GraphAPIError
,
ex
:
...
...
lms/djangoapps/mobile_api/users/serializers.py
View file @
2fd6add5
...
@@ -15,7 +15,7 @@ from xmodule.course_module import DEFAULT_START_DATE
...
@@ -15,7 +15,7 @@ from xmodule.course_module import DEFAULT_START_DATE
class
CourseOverviewField
(
serializers
.
RelatedField
):
class
CourseOverviewField
(
serializers
.
RelatedField
):
"""Custom field to wrap a CourseDescriptor object. Read-only."""
"""Custom field to wrap a CourseDescriptor object. Read-only."""
def
to_
representation
(
self
,
course_overview
):
def
to_
native
(
self
,
course_overview
):
course_id
=
unicode
(
course_overview
.
id
)
course_id
=
unicode
(
course_overview
.
id
)
request
=
self
.
context
.
get
(
'request'
,
None
)
request
=
self
.
context
.
get
(
'request'
,
None
)
if
request
:
if
request
:
...
@@ -77,8 +77,8 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
...
@@ -77,8 +77,8 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
"""
"""
Serializes CourseEnrollment models
Serializes CourseEnrollment models
"""
"""
course
=
CourseOverviewField
(
source
=
"course_overview"
,
read_only
=
True
)
course
=
CourseOverviewField
(
source
=
"course_overview"
)
certificate
=
serializers
.
SerializerMethodField
()
certificate
=
serializers
.
SerializerMethodField
(
'get_certificate'
)
def
get_certificate
(
self
,
model
):
def
get_certificate
(
self
,
model
):
"""Returns the information about the user's certificate in the course."""
"""Returns the information about the user's certificate in the course."""
...
@@ -100,7 +100,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
...
@@ -100,7 +100,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
"""
"""
Serializes User models
Serializes User models
"""
"""
name
=
serializers
.
ReadOnly
Field
(
source
=
'profile.name'
)
name
=
serializers
.
Field
(
source
=
'profile.name'
)
course_enrollments
=
serializers
.
HyperlinkedIdentityField
(
course_enrollments
=
serializers
.
HyperlinkedIdentityField
(
view_name
=
'courseenrollment-detail'
,
view_name
=
'courseenrollment-detail'
,
lookup_field
=
'username'
lookup_field
=
'username'
...
...
lms/djangoapps/mobile_api/users/views.py
View file @
2fd6add5
...
@@ -251,14 +251,6 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
...
@@ -251,14 +251,6 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
serializer_class
=
CourseEnrollmentSerializer
serializer_class
=
CourseEnrollmentSerializer
lookup_field
=
'username'
lookup_field
=
'username'
# In Django Rest Framework v3, there is a default pagination
# class that transmutes the response data into a dictionary
# with pagination information. The original response data (a list)
# is stored in a "results" value of the dictionary.
# For backwards compatibility with the existing API, we disable
# the default behavior by setting the pagination_class to None.
pagination_class
=
None
def
get_queryset
(
self
):
def
get_queryset
(
self
):
enrollments
=
self
.
queryset
.
filter
(
enrollments
=
self
.
queryset
.
filter
(
user__username
=
self
.
kwargs
[
'username'
],
user__username
=
self
.
kwargs
[
'username'
],
...
...
lms/djangoapps/notifier_api/serializers.py
View file @
2fd6add5
...
@@ -23,9 +23,9 @@ class NotifierUserSerializer(serializers.ModelSerializer):
...
@@ -23,9 +23,9 @@ class NotifierUserSerializer(serializers.ModelSerializer):
* course_groups
* course_groups
* roles__permissions
* roles__permissions
"""
"""
name
=
serializers
.
SerializerMethodField
()
name
=
serializers
.
SerializerMethodField
(
"get_name"
)
preferences
=
serializers
.
SerializerMethodField
()
preferences
=
serializers
.
SerializerMethodField
(
"get_preferences"
)
course_info
=
serializers
.
SerializerMethodField
()
course_info
=
serializers
.
SerializerMethodField
(
"get_course_info"
)
def
get_name
(
self
,
user
):
def
get_name
(
self
,
user
):
return
user
.
profile
.
name
return
user
.
profile
.
name
...
...
lms/djangoapps/notifier_api/views.py
View file @
2fd6add5
from
django.contrib.auth.models
import
User
from
django.contrib.auth.models
import
User
from
rest_framework.viewsets
import
ReadOnlyModelViewSet
from
rest_framework.viewsets
import
ReadOnlyModelViewSet
from
rest_framework.response
import
Response
from
rest_framework
import
pagination
from
notification_prefs
import
NOTIFICATION_PREF_KEY
from
notification_prefs
import
NOTIFICATION_PREF_KEY
from
notifier_api.serializers
import
NotifierUserSerializer
from
notifier_api.serializers
import
NotifierUserSerializer
from
openedx.core.lib.api.permissions
import
ApiKeyHeaderPermission
from
openedx.core.lib.api.permissions
import
ApiKeyHeaderPermission
class
NotifierPaginator
(
pagination
.
PageNumberPagination
):
"""
Paginator for the notifier API.
"""
page_size
=
10
page_size_query_param
=
"page_size"
def
get_paginated_response
(
self
,
data
):
"""
Construct a response with pagination information.
"""
return
Response
({
'next'
:
self
.
get_next_link
(),
'previous'
:
self
.
get_previous_link
(),
'count'
:
self
.
page
.
paginator
.
count
,
'results'
:
data
})
class
NotifierUsersViewSet
(
ReadOnlyModelViewSet
):
class
NotifierUsersViewSet
(
ReadOnlyModelViewSet
):
"""
"""
An endpoint that the notifier can use to retrieve users who have enabled
An endpoint that the notifier can use to retrieve users who have enabled
...
@@ -35,7 +14,8 @@ class NotifierUsersViewSet(ReadOnlyModelViewSet):
...
@@ -35,7 +14,8 @@ class NotifierUsersViewSet(ReadOnlyModelViewSet):
"""
"""
permission_classes
=
(
ApiKeyHeaderPermission
,)
permission_classes
=
(
ApiKeyHeaderPermission
,)
serializer_class
=
NotifierUserSerializer
serializer_class
=
NotifierUserSerializer
pagination_class
=
NotifierPaginator
paginate_by
=
10
paginate_by_param
=
"page_size"
# See NotifierUserSerializer for notes about related tables
# See NotifierUserSerializer for notes about related tables
queryset
=
User
.
objects
.
filter
(
queryset
=
User
.
objects
.
filter
(
...
...
lms/djangoapps/teams/models.py
View file @
2fd6add5
...
@@ -3,6 +3,7 @@
...
@@ -3,6 +3,7 @@
from
datetime
import
datetime
from
datetime
import
datetime
from
uuid
import
uuid4
from
uuid
import
uuid4
import
pytz
import
pytz
from
datetime
import
datetime
from
model_utils
import
FieldTracker
from
model_utils
import
FieldTracker
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.core.exceptions
import
ObjectDoesNotExist
...
...
lms/djangoapps/teams/search_indexes.py
View file @
2fd6add5
...
@@ -10,7 +10,6 @@ from django.utils import translation
...
@@ -10,7 +10,6 @@ from django.utils import translation
from
functools
import
wraps
from
functools
import
wraps
from
search.search_engine_base
import
SearchEngine
from
search.search_engine_base
import
SearchEngine
from
request_cache
import
get_request_or_stub
from
.errors
import
ElasticSearchConnectionError
from
.errors
import
ElasticSearchConnectionError
from
.serializers
import
CourseTeamSerializer
,
CourseTeam
from
.serializers
import
CourseTeamSerializer
,
CourseTeam
...
@@ -48,15 +47,7 @@ class CourseTeamIndexer(object):
...
@@ -48,15 +47,7 @@ class CourseTeamIndexer(object):
Returns serialized object with additional search fields.
Returns serialized object with additional search fields.
"""
"""
# Django Rest Framework v3.1 requires that we pass the request to the serializer
serialized_course_team
=
CourseTeamSerializer
(
self
.
course_team
)
.
data
# so it can construct hyperlinks. To avoid changing the interface of this object,
# we retrieve the request from the request cache.
context
=
{
"request"
:
get_request_or_stub
()
}
serialized_course_team
=
CourseTeamSerializer
(
self
.
course_team
,
context
=
context
)
.
data
# Save the primary key so we can load the full objects easily after we search
# Save the primary key so we can load the full objects easily after we search
serialized_course_team
[
'pk'
]
=
self
.
course_team
.
pk
serialized_course_team
[
'pk'
]
=
self
.
course_team
.
pk
# Don't save the membership relations in elasticsearch
# Don't save the membership relations in elasticsearch
...
...
lms/djangoapps/teams/serializers.py
View file @
2fd6add5
...
@@ -4,44 +4,15 @@ from django.contrib.auth.models import User
...
@@ -4,44 +4,15 @@ from django.contrib.auth.models import User
from
django.db.models
import
Count
from
django.db.models
import
Count
from
django.conf
import
settings
from
django.conf
import
settings
from
django_countries
import
countries
from
rest_framework
import
serializers
from
rest_framework
import
serializers
from
openedx.core.lib.api.serializers
import
CollapsedReferenceSerializer
from
openedx.core.lib.api.serializers
import
CollapsedReferenceSerializer
,
PaginationSerializer
from
openedx.core.lib.api.fields
import
ExpandableField
from
openedx.core.lib.api.fields
import
ExpandableField
from
openedx.core.djangoapps.user_api.accounts.serializers
import
UserReadOnlySerializer
from
openedx.core.djangoapps.user_api.accounts.serializers
import
UserReadOnlySerializer
from
.models
import
CourseTeam
,
CourseTeamMembership
from
.models
import
CourseTeam
,
CourseTeamMembership
class
CountryField
(
serializers
.
Field
):
"""
Field to serialize a country code.
"""
COUNTRY_CODES
=
dict
(
countries
)
.
keys
()
def
to_representation
(
self
,
obj
):
"""
Represent the country as a 2-character unicode identifier.
"""
return
unicode
(
obj
)
def
to_internal_value
(
self
,
data
):
"""
Check that the code is a valid country code.
We leave the data in its original format so that the Django model's
CountryField can convert it to the internal representation used
by the django-countries library.
"""
if
data
and
data
not
in
self
.
COUNTRY_CODES
:
raise
serializers
.
ValidationError
(
u"{code} is not a valid country code"
.
format
(
code
=
data
)
)
return
data
class
UserMembershipSerializer
(
serializers
.
ModelSerializer
):
class
UserMembershipSerializer
(
serializers
.
ModelSerializer
):
"""Serializes CourseTeamMemberships with only user and date_joined
"""Serializes CourseTeamMemberships with only user and date_joined
...
@@ -72,7 +43,6 @@ class CourseTeamSerializer(serializers.ModelSerializer):
...
@@ -72,7 +43,6 @@ class CourseTeamSerializer(serializers.ModelSerializer):
"""Serializes a CourseTeam with membership information."""
"""Serializes a CourseTeam with membership information."""
id
=
serializers
.
CharField
(
source
=
'team_id'
,
read_only
=
True
)
# pylint: disable=invalid-name
id
=
serializers
.
CharField
(
source
=
'team_id'
,
read_only
=
True
)
# pylint: disable=invalid-name
membership
=
UserMembershipSerializer
(
many
=
True
,
read_only
=
True
)
membership
=
UserMembershipSerializer
(
many
=
True
,
read_only
=
True
)
country
=
CountryField
()
class
Meta
(
object
):
class
Meta
(
object
):
"""Defines meta information for the ModelSerializer."""
"""Defines meta information for the ModelSerializer."""
...
@@ -96,8 +66,6 @@ class CourseTeamSerializer(serializers.ModelSerializer):
...
@@ -96,8 +66,6 @@ class CourseTeamSerializer(serializers.ModelSerializer):
class
CourseTeamCreationSerializer
(
serializers
.
ModelSerializer
):
class
CourseTeamCreationSerializer
(
serializers
.
ModelSerializer
):
"""Deserializes a CourseTeam for creation."""
"""Deserializes a CourseTeam for creation."""
country
=
CountryField
(
required
=
False
)
class
Meta
(
object
):
class
Meta
(
object
):
"""Defines meta information for the ModelSerializer."""
"""Defines meta information for the ModelSerializer."""
model
=
CourseTeam
model
=
CourseTeam
...
@@ -110,17 +78,16 @@ class CourseTeamCreationSerializer(serializers.ModelSerializer):
...
@@ -110,17 +78,16 @@ class CourseTeamCreationSerializer(serializers.ModelSerializer):
"language"
,
"language"
,
)
)
def
create
(
self
,
validated_data
):
def
restore_object
(
self
,
attrs
,
instance
=
None
):
team
=
CourseTeam
.
create
(
"""Restores a CourseTeam instance from the given attrs."""
name
=
validated_data
.
get
(
"name"
,
''
),
return
CourseTeam
.
create
(
course_id
=
validated_data
.
get
(
"course_id"
),
name
=
attrs
.
get
(
"name"
,
''
),
description
=
validated_data
.
get
(
"description"
,
''
),
course_id
=
attrs
.
get
(
"course_id"
),
topic_id
=
validated_data
.
get
(
"topic_id"
,
''
),
description
=
attrs
.
get
(
"description"
,
''
),
country
=
validated_data
.
get
(
"country"
,
''
),
topic_id
=
attrs
.
get
(
"topic_id"
,
''
),
language
=
validated_data
.
get
(
"language"
,
''
),
country
=
attrs
.
get
(
"country"
,
''
),
language
=
attrs
.
get
(
"language"
,
''
),
)
)
team
.
save
()
return
team
class
CourseTeamSerializerWithoutMembership
(
CourseTeamSerializer
):
class
CourseTeamSerializerWithoutMembership
(
CourseTeamSerializer
):
...
@@ -167,6 +134,13 @@ class MembershipSerializer(serializers.ModelSerializer):
...
@@ -167,6 +134,13 @@ class MembershipSerializer(serializers.ModelSerializer):
read_only_fields
=
(
"date_joined"
,
"last_activity_at"
)
read_only_fields
=
(
"date_joined"
,
"last_activity_at"
)
class
PaginatedMembershipSerializer
(
PaginationSerializer
):
"""Serializes team memberships with support for pagination."""
class
Meta
(
object
):
"""Defines meta information for the PaginatedMembershipSerializer."""
object_serializer_class
=
MembershipSerializer
class
BaseTopicSerializer
(
serializers
.
Serializer
):
class
BaseTopicSerializer
(
serializers
.
Serializer
):
"""Serializes a topic without team_count."""
"""Serializes a topic without team_count."""
description
=
serializers
.
CharField
()
description
=
serializers
.
CharField
()
...
@@ -181,7 +155,7 @@ class TopicSerializer(BaseTopicSerializer):
...
@@ -181,7 +155,7 @@ class TopicSerializer(BaseTopicSerializer):
model to get the count. Requires that `context` is provided with a valid course_id
model to get the count. Requires that `context` is provided with a valid course_id
in order to filter teams within the course.
in order to filter teams within the course.
"""
"""
team_count
=
serializers
.
SerializerMethodField
()
team_count
=
serializers
.
SerializerMethodField
(
'get_team_count'
)
def
get_team_count
(
self
,
topic
):
def
get_team_count
(
self
,
topic
):
"""Get the number of teams associated with this topic"""
"""Get the number of teams associated with this topic"""
...
@@ -192,25 +166,31 @@ class TopicSerializer(BaseTopicSerializer):
...
@@ -192,25 +166,31 @@ class TopicSerializer(BaseTopicSerializer):
return
CourseTeam
.
objects
.
filter
(
course_id
=
self
.
context
[
'course_id'
],
topic_id
=
topic
[
'id'
])
.
count
()
return
CourseTeam
.
objects
.
filter
(
course_id
=
self
.
context
[
'course_id'
],
topic_id
=
topic
[
'id'
])
.
count
()
class
BulkTeamCountTopicListSerializer
(
serializers
.
ListSerializer
):
# pylint: disable=abstract-method
class
PaginatedTopicSerializer
(
PaginationSerializer
):
"""
"""
List serializer for efficiently serializing a set of topics.
Serializes a set of topics, adding the team_count field to each topic individually, if team_count
is not already present in the topic data. Requires that `context` is provided with a valid course_id in
order to filter teams within the course.
"""
"""
class
Meta
(
object
):
def
to_representation
(
self
,
obj
):
"""Defines meta information for the PaginatedTopicSerializer."""
"""Adds team_count to each topic. """
object_serializer_class
=
TopicSerializer
data
=
super
(
BulkTeamCountTopicListSerializer
,
self
)
.
to_representation
(
obj
)
add_team_count
(
data
,
self
.
context
[
"course_id"
])
return
data
class
BulkTeamCount
TopicSerializer
(
BaseTopicSerializer
):
# pylint: disable=abstract-method
class
BulkTeamCount
PaginatedTopicSerializer
(
PaginationSerializer
):
"""
"""
Serializes a set of topics, adding the team_count field to each topic as a bulk operation.
Serializes a set of topics, adding the team_count field to each topic as a bulk operation per page
Requires that `context` is provided with a valid course_id in order to filter teams within the course.
(only on the page being returned). Requires that `context` is provided with a valid course_id in
order to filter teams within the course.
"""
"""
class
Meta
:
# pylint: disable=missing-docstring,old-style-class
class
Meta
(
object
):
list_serializer_class
=
BulkTeamCountTopicListSerializer
"""Defines meta information for the BulkTeamCountPaginatedTopicSerializer."""
object_serializer_class
=
BaseTopicSerializer
def
__init__
(
self
,
*
args
,
**
kwargs
):
"""Adds team_count to each topic on the current page."""
super
(
BulkTeamCountPaginatedTopicSerializer
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
add_team_count
(
self
.
data
[
'results'
],
self
.
context
[
'course_id'
])
def
add_team_count
(
topics
,
course_id
):
def
add_team_count
(
topics
,
course_id
):
...
...
lms/djangoapps/teams/tests/factories.py
View file @
2fd6add5
...
@@ -34,10 +34,3 @@ class CourseTeamMembershipFactory(DjangoModelFactory):
...
@@ -34,10 +34,3 @@ class CourseTeamMembershipFactory(DjangoModelFactory):
class
Meta
(
object
):
# pylint: disable=missing-docstring
class
Meta
(
object
):
# pylint: disable=missing-docstring
model
=
CourseTeamMembership
model
=
CourseTeamMembership
last_activity_at
=
LAST_ACTIVITY_AT
last_activity_at
=
LAST_ACTIVITY_AT
@classmethod
def
_create
(
cls
,
model_class
,
*
args
,
**
kwargs
):
"""Create the team membership. """
obj
=
model_class
(
*
args
,
**
kwargs
)
obj
.
save
()
return
obj
lms/djangoapps/teams/tests/test_serializers.py
View file @
2fd6add5
...
@@ -11,9 +11,12 @@ from xmodule.modulestore.tests.factories import CourseFactory
...
@@ -11,9 +11,12 @@ from xmodule.modulestore.tests.factories import CourseFactory
from
lms.djangoapps.teams.tests.factories
import
CourseTeamFactory
,
CourseTeamMembershipFactory
from
lms.djangoapps.teams.tests.factories
import
CourseTeamFactory
,
CourseTeamMembershipFactory
from
lms.djangoapps.teams.serializers
import
(
from
lms.djangoapps.teams.serializers
import
(
BulkTeamCountTopicSerializer
,
BaseTopicSerializer
,
PaginatedTopicSerializer
,
BulkTeamCountPaginatedTopicSerializer
,
TopicSerializer
,
TopicSerializer
,
MembershipSerializer
,
MembershipSerializer
,
add_team_count
)
)
...
@@ -70,6 +73,21 @@ class MembershipSerializerTestCase(SerializerTestCase):
...
@@ -70,6 +73,21 @@ class MembershipSerializerTestCase(SerializerTestCase):
self
.
assertNotIn
(
'membership'
,
data
[
'team'
])
self
.
assertNotIn
(
'membership'
,
data
[
'team'
])
class
BaseTopicSerializerTestCase
(
SerializerTestCase
):
"""
Tests for the `BaseTopicSerializer`, which should not serialize team count
data.
"""
def
test_team_count_not_included
(
self
):
"""Verifies that the `BaseTopicSerializer` does not include team count"""
with
self
.
assertNumQueries
(
0
):
serializer
=
BaseTopicSerializer
(
self
.
course
.
teams_topics
[
0
])
self
.
assertEqual
(
serializer
.
data
,
{
u'name'
:
u'Tøpic'
,
u'description'
:
u'The bést topic!'
,
u'id'
:
u'0'
}
)
class
TopicSerializerTestCase
(
SerializerTestCase
):
class
TopicSerializerTestCase
(
SerializerTestCase
):
"""
"""
Tests for the `TopicSerializer`, which should serialize team count data for
Tests for the `TopicSerializer`, which should serialize team count data for
...
@@ -119,7 +137,7 @@ class TopicSerializerTestCase(SerializerTestCase):
...
@@ -119,7 +137,7 @@ class TopicSerializerTestCase(SerializerTestCase):
)
)
class
BaseTopicSerializerTestCase
(
SerializerTestCase
):
class
Base
Paginated
TopicSerializerTestCase
(
SerializerTestCase
):
"""
"""
Base class for testing the two paginated topic serializers.
Base class for testing the two paginated topic serializers.
"""
"""
...
@@ -173,15 +191,13 @@ class BaseTopicSerializerTestCase(SerializerTestCase):
...
@@ -173,15 +191,13 @@ class BaseTopicSerializerTestCase(SerializerTestCase):
self
.
assert_serializer_output
([],
num_teams_per_topic
=
0
,
num_queries
=
0
)
self
.
assert_serializer_output
([],
num_teams_per_topic
=
0
,
num_queries
=
0
)
class
BulkTeamCount
TopicSerializerTestCase
(
Base
TopicSerializerTestCase
):
class
BulkTeamCount
PaginatedTopicSerializerTestCase
(
BasePaginated
TopicSerializerTestCase
):
"""
"""
Tests for the `BulkTeamCountTopicSerializer`, which should serialize team_count
Tests for the `BulkTeamCount
Paginated
TopicSerializer`, which should serialize team_count
data for many topics with constant time SQL queries.
data for many topics with constant time SQL queries.
"""
"""
__test__
=
True
__test__
=
True
serializer
=
BulkTeamCountTopicSerializer
serializer
=
BulkTeamCountPaginatedTopicSerializer
NUM_TOPICS
=
6
def
test_topics_with_no_team_counts
(
self
):
def
test_topics_with_no_team_counts
(
self
):
"""
"""
...
@@ -206,13 +222,13 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
...
@@ -206,13 +222,13 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
one SQL query.
one SQL query.
"""
"""
teams_per_topic
=
10
teams_per_topic
=
10
topics
=
self
.
setup_topics
(
num_topics
=
self
.
NUM_TOPICS
,
teams_per_topic
=
teams_per_topic
)
topics
=
self
.
setup_topics
(
num_topics
=
self
.
PAGE_SIZE
+
1
,
teams_per_topic
=
teams_per_topic
)
self
.
assert_serializer_output
(
topics
,
num_teams_per_topic
=
teams_per_topic
,
num_queries
=
1
)
self
.
assert_serializer_output
(
topics
[:
self
.
PAGE_SIZE
]
,
num_teams_per_topic
=
teams_per_topic
,
num_queries
=
1
)
def
test_scoped_within_course
(
self
):
def
test_scoped_within_course
(
self
):
"""Verify that team counts are scoped within a course."""
"""Verify that team counts are scoped within a course."""
teams_per_topic
=
10
teams_per_topic
=
10
first_course_topics
=
self
.
setup_topics
(
num_topics
=
self
.
NUM_TOPICS
,
teams_per_topic
=
teams_per_topic
)
first_course_topics
=
self
.
setup_topics
(
num_topics
=
self
.
PAGE_SIZE
,
teams_per_topic
=
teams_per_topic
)
duplicate_topic
=
first_course_topics
[
0
]
duplicate_topic
=
first_course_topics
[
0
]
second_course
=
CourseFactory
.
create
(
second_course
=
CourseFactory
.
create
(
teams_configuration
=
{
teams_configuration
=
{
...
@@ -223,44 +239,27 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
...
@@ -223,44 +239,27 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
CourseTeamFactory
.
create
(
course_id
=
second_course
.
id
,
topic_id
=
duplicate_topic
[
u'id'
])
CourseTeamFactory
.
create
(
course_id
=
second_course
.
id
,
topic_id
=
duplicate_topic
[
u'id'
])
self
.
assert_serializer_output
(
first_course_topics
,
num_teams_per_topic
=
teams_per_topic
,
num_queries
=
1
)
self
.
assert_serializer_output
(
first_course_topics
,
num_teams_per_topic
=
teams_per_topic
,
num_queries
=
1
)
def
_merge_dicts
(
self
,
first
,
second
):
"""Convenience method to merge two dicts in a single expression"""
result
=
first
.
copy
()
result
.
update
(
second
)
return
result
def
setup_topics
(
self
,
num_topics
=
5
,
teams_per_topic
=
0
):
class
PaginatedTopicSerializerTestCase
(
BasePaginatedTopicSerializerTestCase
):
"""
"""
Helper method to set up topics on the course. Returns a list of
Tests for the `PaginatedTopicSerializer`, which will add team_count information per topic if not present.
created topics.
"""
"""
__test__
=
True
self
.
course
.
teams_configuration
[
'topics'
]
=
[]
serializer
=
PaginatedTopicSerializer
topics
=
[
{
u'name'
:
u'Tøpic {}'
.
format
(
i
),
u'description'
:
u'The bést topic! {}'
.
format
(
i
),
u'id'
:
unicode
(
i
)}
for
i
in
xrange
(
num_topics
)
]
for
i
in
xrange
(
num_topics
):
topic_id
=
unicode
(
i
)
self
.
course
.
teams_configuration
[
'topics'
]
.
append
(
topics
[
i
])
for
_
in
xrange
(
teams_per_topic
):
CourseTeamFactory
.
create
(
course_id
=
self
.
course
.
id
,
topic_id
=
topic_id
)
return
topics
def
assert_serializer_output
(
self
,
topics
,
num_teams_per_topic
,
num_queries
):
def
test_topics_with_team_counts
(
self
):
"""
"""
Verify that
the serializer produced the expected topics
.
Verify that
we serialize topics with team_count, making one SQL query per topic
.
"""
"""
with
self
.
assertNumQueries
(
num_queries
):
teams_per_topic
=
2
serializer
=
self
.
serializer
(
topics
,
context
=
{
'course_id'
:
self
.
course
.
id
},
many
=
True
)
topics
=
self
.
setup_topics
(
teams_per_topic
=
teams_per_topic
)
self
.
assertEqual
(
self
.
assert_serializer_output
(
topics
,
num_teams_per_topic
=
teams_per_topic
,
num_queries
=
5
)
serializer
.
data
,
[
self
.
_merge_dicts
(
topic
,
{
u'team_count'
:
num_teams_per_topic
})
for
topic
in
topics
]
)
def
test_
no_topics
(
self
):
def
test_
topics_with_team_counts_prepopulated
(
self
):
"""
"""
Verify that we return no results and make no SQL queries for a page
Verify that if team_count is pre-populated, there are no additional SQL queries.
with no topics.
"""
"""
self
.
course
.
teams_configuration
[
'topics'
]
=
[]
teams_per_topic
=
8
self
.
assert_serializer_output
([],
num_teams_per_topic
=
0
,
num_queries
=
0
)
topics
=
self
.
setup_topics
(
teams_per_topic
=
teams_per_topic
)
add_team_count
(
topics
,
self
.
course
.
id
)
self
.
assert_serializer_output
(
topics
,
num_teams_per_topic
=
teams_per_topic
,
num_queries
=
0
)
lms/djangoapps/teams/tests/test_views.py
View file @
2fd6add5
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
"""Tests for the teams API at the HTTP request level."""
"""Tests for the teams API at the HTTP request level."""
import
json
import
json
from
datetime
import
datetime
import
pytz
import
pytz
from
datetime
import
datetime
from
dateutil
import
parser
from
dateutil
import
parser
import
ddt
import
ddt
from
elasticsearch.exceptions
import
ConnectionError
from
elasticsearch.exceptions
import
ConnectionError
from
mock
import
patch
from
mock
import
patch
from
search.search_engine_base
import
SearchEngine
from
search.search_engine_base
import
SearchEngine
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.conf
import
settings
from
django.conf
import
settings
from
django.db.models.signals
import
post_save
from
django.db.models.signals
import
post_save
from
django.utils
import
translation
from
django.utils
import
translation
from
nose.plugins.attrib
import
attr
from
nose.plugins.attrib
import
attr
from
rest_framework.test
import
APITestCase
,
APIClient
from
rest_framework.test
import
APITestCase
,
APIClient
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
courseware.tests.factories
import
StaffFactory
from
courseware.tests.factories
import
StaffFactory
from
common.test.utils
import
skip_signal
from
common.test.utils
import
skip_signal
from
student.tests.factories
import
UserFactory
,
AdminFactory
,
CourseEnrollmentFactory
from
student.tests.factories
import
UserFactory
,
AdminFactory
,
CourseEnrollmentFactory
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
from
util.testing
import
EventTestMixin
from
util.testing
import
EventTestMixin
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
.factories
import
CourseTeamFactory
,
LAST_ACTIVITY_AT
from
.factories
import
CourseTeamFactory
,
LAST_ACTIVITY_AT
from
..models
import
CourseTeamMembership
from
..models
import
CourseTeamMembership
from
..search_indexes
import
CourseTeamIndexer
,
CourseTeam
,
course_team_post_save_callback
from
..search_indexes
import
CourseTeamIndexer
,
CourseTeam
,
course_team_post_save_callback
from
django_comment_common.models
import
Role
,
FORUM_ROLE_COMMUNITY_TA
from
django_comment_common.models
import
Role
,
FORUM_ROLE_COMMUNITY_TA
from
django_comment_common.utils
import
seed_permissions_roles
from
django_comment_common.utils
import
seed_permissions_roles
...
@@ -35,23 +36,11 @@ class TestDashboard(SharedModuleStoreTestCase):
...
@@ -35,23 +36,11 @@ class TestDashboard(SharedModuleStoreTestCase):
"""Tests for the Teams dashboard."""
"""Tests for the Teams dashboard."""
test_password
=
"test"
test_password
=
"test"
NUM_TOPICS
=
10
@classmethod
@classmethod
def
setUpClass
(
cls
):
def
setUpClass
(
cls
):
super
(
TestDashboard
,
cls
)
.
setUpClass
()
super
(
TestDashboard
,
cls
)
.
setUpClass
()
cls
.
course
=
CourseFactory
.
create
(
cls
.
course
=
CourseFactory
.
create
(
teams_configuration
=
{
teams_configuration
=
{
"max_team_size"
:
10
,
"topics"
:
[{
"name"
:
"foo"
,
"id"
:
0
,
"description"
:
"test topic"
}]}
"max_team_size"
:
10
,
"topics"
:
[
{
"name"
:
"Topic {}"
.
format
(
topic_id
),
"id"
:
topic_id
,
"description"
:
"Description for topic {}"
.
format
(
topic_id
)
}
for
topic_id
in
range
(
cls
.
NUM_TOPICS
)
]
}
)
)
def
setUp
(
self
):
def
setUp
(
self
):
...
@@ -108,30 +97,6 @@ class TestDashboard(SharedModuleStoreTestCase):
...
@@ -108,30 +97,6 @@ class TestDashboard(SharedModuleStoreTestCase):
response
=
self
.
client
.
get
(
teams_url
)
response
=
self
.
client
.
get
(
teams_url
)
self
.
assertEqual
(
404
,
response
.
status_code
)
self
.
assertEqual
(
404
,
response
.
status_code
)
def
test_query_counts
(
self
):
# Enroll in the course and log in
CourseEnrollmentFactory
.
create
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
# Check the query count on the dashboard With no teams
with
self
.
assertNumQueries
(
15
):
self
.
client
.
get
(
self
.
teams_url
)
# Create some teams
for
topic_id
in
range
(
self
.
NUM_TOPICS
):
team
=
CourseTeamFactory
.
create
(
name
=
u"Team for topic {}"
.
format
(
topic_id
),
course_id
=
self
.
course
.
id
,
topic_id
=
topic_id
,
)
# Add the user to the last team
team
.
add_user
(
self
.
user
)
# Check the query count on the dashboard again
with
self
.
assertNumQueries
(
19
):
self
.
client
.
get
(
self
.
teams_url
)
def
test_bad_course_id
(
self
):
def
test_bad_course_id
(
self
):
"""
"""
Verifies expected behavior when course_id does not reference an existing course or is invalid.
Verifies expected behavior when course_id does not reference an existing course or is invalid.
...
@@ -287,9 +252,6 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase):
...
@@ -287,9 +252,6 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase):
self
.
users
[
user
],
course
.
id
,
check_access
=
True
self
.
users
[
user
],
course
.
id
,
check_access
=
True
)
)
# Django Rest Framework v3 requires us to pass a request to serializers
# that have URL fields. Since we're invoking this code outside the context
# of a request, we need to simulate that there's a request.
self
.
solar_team
.
add_user
(
self
.
users
[
'student_enrolled'
])
self
.
solar_team
.
add_user
(
self
.
users
[
'student_enrolled'
])
self
.
nuclear_team
.
add_user
(
self
.
users
[
'student_enrolled_both_courses_other_team'
])
self
.
nuclear_team
.
add_user
(
self
.
users
[
'student_enrolled_both_courses_other_team'
])
self
.
another_team
.
add_user
(
self
.
users
[
'student_enrolled_both_courses_other_team'
])
self
.
another_team
.
add_user
(
self
.
users
[
'student_enrolled_both_courses_other_team'
])
...
@@ -349,17 +311,7 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase):
...
@@ -349,17 +311,7 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase):
response
=
func
(
url
,
data
=
data
,
content_type
=
content_type
)
response
=
func
(
url
,
data
=
data
,
content_type
=
content_type
)
else
:
else
:
response
=
func
(
url
,
data
=
data
)
response
=
func
(
url
,
data
=
data
)
self
.
assertEqual
(
expected_status
,
response
.
status_code
)
self
.
assertEqual
(
expected_status
,
response
.
status_code
,
msg
=
"Expected status {expected} but got {actual}: {content}"
.
format
(
expected
=
expected_status
,
actual
=
response
.
status_code
,
content
=
response
.
content
,
)
)
if
expected_status
==
200
:
if
expected_status
==
200
:
return
json
.
loads
(
response
.
content
)
return
json
.
loads
(
response
.
content
)
else
:
else
:
...
...
lms/djangoapps/teams/views.py
View file @
2fd6add5
...
@@ -5,14 +5,19 @@ import logging
...
@@ -5,14 +5,19 @@ import logging
from
django.shortcuts
import
get_object_or_404
,
render_to_response
from
django.shortcuts
import
get_object_or_404
,
render_to_response
from
django.http
import
Http404
from
django.http
import
Http404
from
django.conf
import
settings
from
django.conf
import
settings
from
django.core.paginator
import
Paginator
from
django.views.generic.base
import
View
from
rest_framework.generics
import
GenericAPIView
from
rest_framework.generics
import
GenericAPIView
from
rest_framework.response
import
Response
from
rest_framework.response
import
Response
from
rest_framework.reverse
import
reverse
from
rest_framework.reverse
import
reverse
from
rest_framework.views
import
APIView
from
rest_framework.views
import
APIView
from
rest_framework.authentication
import
SessionAuthentication
from
rest_framework.authentication
import
(
from
rest_framework_oauth.authentication
import
OAuth2Authentication
SessionAuthentication
,
OAuth2Authentication
)
from
rest_framework
import
status
from
rest_framework
import
status
from
rest_framework
import
permissions
from
rest_framework
import
permissions
from
django.db.models
import
Count
from
django.db.models.signals
import
post_save
from
django.db.models.signals
import
post_save
from
django.dispatch
import
receiver
from
django.dispatch
import
receiver
from
django.contrib.auth.models
import
User
from
django.contrib.auth.models
import
User
...
@@ -27,7 +32,8 @@ from openedx.core.lib.api.view_utils import (
...
@@ -27,7 +32,8 @@ from openedx.core.lib.api.view_utils import (
build_api_error
,
build_api_error
,
ExpandableFieldViewMixin
ExpandableFieldViewMixin
)
)
from
openedx.core.lib.api.paginators
import
paginate_search_results
,
DefaultPagination
from
openedx.core.lib.api.serializers
import
PaginationSerializer
from
openedx.core.lib.api.paginators
import
paginate_search_results
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
opaque_keys
import
InvalidKeyError
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
...
@@ -43,8 +49,10 @@ from .serializers import (
...
@@ -43,8 +49,10 @@ from .serializers import (
CourseTeamSerializer
,
CourseTeamSerializer
,
CourseTeamCreationSerializer
,
CourseTeamCreationSerializer
,
TopicSerializer
,
TopicSerializer
,
BulkTeamCountTopicSerializer
,
PaginatedTopicSerializer
,
BulkTeamCountPaginatedTopicSerializer
,
MembershipSerializer
,
MembershipSerializer
,
PaginatedMembershipSerializer
,
add_team_count
add_team_count
)
)
from
.search_indexes
import
CourseTeamIndexer
from
.search_indexes
import
CourseTeamIndexer
...
@@ -77,42 +85,7 @@ def team_post_save_callback(sender, instance, **kwargs): # pylint: disable=unus
...
@@ -77,42 +85,7 @@ def team_post_save_callback(sender, instance, **kwargs): # pylint: disable=unus
)
)
class
TeamAPIPagination
(
DefaultPagination
):
class
TeamsDashboardView
(
View
):
"""
Pagination format used by the teams API.
"""
page_size_query_param
=
"page_size"
def
get_paginated_response
(
self
,
data
):
"""
Annotate the response with pagination information.
"""
response
=
super
(
TeamAPIPagination
,
self
)
.
get_paginated_response
(
data
)
# Add the current page to the response.
# It may make sense to eventually move this field into the default
# implementation, but for now, teams is the only API that uses this.
response
.
data
[
"current_page"
]
=
self
.
page
.
number
# This field can be derived from other fields in the response,
# so it may make sense to have the JavaScript client calculate it
# instead of including it in the response.
response
.
data
[
"start"
]
=
(
self
.
page
.
number
-
1
)
*
self
.
get_page_size
(
self
.
request
)
return
response
class
TopicsPagination
(
TeamAPIPagination
):
"""Paginate topics. """
page_size
=
TOPICS_PER_PAGE
class
MembershipPagination
(
TeamAPIPagination
):
"""Paginate memberships. """
page_size
=
TEAM_MEMBERSHIPS_PER_PAGE
class
TeamsDashboardView
(
GenericAPIView
):
"""
"""
View methods related to the teams dashboard.
View methods related to the teams dashboard.
"""
"""
...
@@ -134,38 +107,29 @@ class TeamsDashboardView(GenericAPIView):
...
@@ -134,38 +107,29 @@ class TeamsDashboardView(GenericAPIView):
not
has_access
(
request
.
user
,
'staff'
,
course
,
course
.
id
):
not
has_access
(
request
.
user
,
'staff'
,
course
,
course
.
id
):
raise
Http404
raise
Http404
user
=
request
.
user
# Even though sorting is done outside of the serializer, sort_order needs to be passed
# Even though sorting is done outside of the serializer, sort_order needs to be passed
# to the serializer so that the paginated results indicate how they were sorted.
# to the serializer so that the paginated results indicate how they were sorted.
sort_order
=
'name'
sort_order
=
'name'
topics
=
get_alphabetical_topics
(
course
)
topics
=
get_alphabetical_topics
(
course
)
topics_page
=
Paginator
(
topics
,
TOPICS_PER_PAGE
)
.
page
(
1
)
# Paginate and serialize topic data
# BulkTeamCountPaginatedTopicSerializer will add team counts to the topics in a single
# BulkTeamCountPaginatedTopicSerializer will add team counts to the topics in a single
# bulk operation per page.
# bulk operation per page.
topics_data
=
self
.
_serialize_and_paginate
(
topics_serializer
=
BulkTeamCountPaginatedTopicSerializer
(
TopicsPagination
,
instance
=
topics_page
,
topics
,
context
=
{
'course_id'
:
course
.
id
,
'sort_order'
:
sort_order
}
request
,
BulkTeamCountTopicSerializer
,
{
'course_id'
:
course
.
id
},
)
)
topics_data
[
"sort_order"
]
=
sort_order
user
=
request
.
user
# Paginate and serialize team membership data.
team_memberships
=
CourseTeamMembership
.
get_memberships
(
request
.
user
.
username
,
[
course
.
id
])
team_memberships
=
CourseTeamMembership
.
get_memberships
(
user
.
username
,
[
course
.
id
])
team_memberships_page
=
Paginator
(
team_memberships
,
TEAM_MEMBERSHIPS_PER_PAGE
)
.
page
(
1
)
memberships_data
=
self
.
_serialize_and_paginate
(
team_memberships_serializer
=
PaginatedMembershipSerializer
(
MembershipPagination
,
instance
=
team_memberships_page
,
team_memberships
,
context
=
{
'expand'
:
(
'team'
,
'user'
),
'request'
:
request
},
request
,
MembershipSerializer
,
{
'expand'
:
(
'team'
,
'user'
,)}
)
)
context
=
{
context
=
{
"course"
:
course
,
"course"
:
course
,
"topics"
:
topics_data
,
"topics"
:
topics_
serializer
.
data
,
# It is necessary to pass both privileged and staff because only privileged users can
# It is necessary to pass both privileged and staff because only privileged users can
# administer discussion threads, but both privileged and staff users are allowed to create
# administer discussion threads, but both privileged and staff users are allowed to create
# multiple teams (since they are not automatically added to teams upon creation).
# multiple teams (since they are not automatically added to teams upon creation).
...
@@ -173,7 +137,7 @@ class TeamsDashboardView(GenericAPIView):
...
@@ -173,7 +137,7 @@ class TeamsDashboardView(GenericAPIView):
"username"
:
user
.
username
,
"username"
:
user
.
username
,
"privileged"
:
has_discussion_privileges
(
user
,
course_key
),
"privileged"
:
has_discussion_privileges
(
user
,
course_key
),
"staff"
:
bool
(
has_access
(
user
,
'staff'
,
course_key
)),
"staff"
:
bool
(
has_access
(
user
,
'staff'
,
course_key
)),
"team_memberships_data"
:
memberships_
data
,
"team_memberships_data"
:
team_memberships_serializer
.
data
,
},
},
"topic_url"
:
reverse
(
"topic_url"
:
reverse
(
'topics_detail'
,
kwargs
=
{
'topic_id'
:
'topic_id'
,
'course_id'
:
str
(
course_id
)},
request
=
request
'topics_detail'
,
kwargs
=
{
'topic_id'
:
'topic_id'
,
'course_id'
:
str
(
course_id
)},
request
=
request
...
@@ -190,39 +154,6 @@ class TeamsDashboardView(GenericAPIView):
...
@@ -190,39 +154,6 @@ class TeamsDashboardView(GenericAPIView):
}
}
return
render_to_response
(
"teams/teams.html"
,
context
)
return
render_to_response
(
"teams/teams.html"
,
context
)
def
_serialize_and_paginate
(
self
,
pagination_cls
,
queryset
,
request
,
serializer_cls
,
serializer_ctx
):
"""
Serialize and paginate objects in a queryset.
Arguments:
pagination_cls (pagination.Paginator class): Django Rest Framework Paginator subclass.
queryset (QuerySet): Django queryset to serialize/paginate.
serializer_cls (serializers.Serializer class): Django Rest Framework Serializer subclass.
serializer_ctx (dict): Context dictionary to pass to the serializer
Returns: dict
"""
# Django Rest Framework v3 requires that we pass the request
# into the serializer's context if the serialize contains
# hyperlink fields.
serializer_ctx
[
"request"
]
=
request
# Instantiate the paginator and use it to paginate the queryset
paginator
=
pagination_cls
()
page
=
paginator
.
paginate_queryset
(
queryset
,
request
)
# Serialize the page
serializer
=
serializer_cls
(
page
,
context
=
serializer_ctx
,
many
=
True
)
# Use the paginator to construct the response data
# This will use the pagination subclass for the view to add additional
# fields to the response.
# For example, if the input data is a list, the output data would
# be a dictionary with keys "count", "next", "previous", and "results"
# (where "results" is set to the value of the original list)
return
paginator
.
get_paginated_response
(
serializer
.
data
)
.
data
def
has_team_api_access
(
user
,
course_key
,
access_username
=
None
):
def
has_team_api_access
(
user
,
course_key
,
access_username
=
None
):
"""Returns True if the user has access to the Team API for the course
"""Returns True if the user has access to the Team API for the course
...
@@ -376,8 +307,11 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
...
@@ -376,8 +307,11 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
# OAuth2Authentication must come first to return a 401 for unauthenticated users
# OAuth2Authentication must come first to return a 401 for unauthenticated users
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
paginate_by
=
10
paginate_by_param
=
'page_size'
pagination_serializer_class
=
PaginationSerializer
serializer_class
=
CourseTeamSerializer
serializer_class
=
CourseTeamSerializer
pagination_class
=
TeamAPIPagination
def
get
(
self
,
request
):
def
get
(
self
,
request
):
"""GET /api/team/v0/teams/"""
"""GET /api/team/v0/teams/"""
...
@@ -443,18 +377,15 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
...
@@ -443,18 +377,15 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
paginated_results
=
paginate_search_results
(
paginated_results
=
paginate_search_results
(
CourseTeam
,
CourseTeam
,
search_results
,
search_results
,
self
.
paginator
.
get_page_size
(
request
),
self
.
get_paginate_by
(
),
self
.
get_page
()
self
.
get_page
()
)
)
serializer
=
self
.
get_pagination_serializer
(
paginated_results
)
emit_team_event
(
'edx.team.searched'
,
course_key
,
{
emit_team_event
(
'edx.team.searched'
,
course_key
,
{
"number_of_results"
:
search_results
[
'total'
],
"number_of_results"
:
search_results
[
'total'
],
"search_text"
:
text_search
,
"search_text"
:
text_search
,
"topic_id"
:
topic_id
,
"topic_id"
:
topic_id
,
})
})
page
=
self
.
paginate_queryset
(
paginated_results
)
serializer
=
self
.
get_serializer
(
page
,
many
=
True
)
order_by_input
=
None
else
:
else
:
queryset
=
CourseTeam
.
objects
.
filter
(
**
result_filter
)
queryset
=
CourseTeam
.
objects
.
filter
(
**
result_filter
)
order_by_input
=
request
.
QUERY_PARAMS
.
get
(
'order_by'
,
'name'
)
order_by_input
=
request
.
QUERY_PARAMS
.
get
(
'order_by'
,
'name'
)
...
@@ -476,12 +407,10 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
...
@@ -476,12 +407,10 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
},
status
=
status
.
HTTP_400_BAD_REQUEST
)
},
status
=
status
.
HTTP_400_BAD_REQUEST
)
page
=
self
.
paginate_queryset
(
queryset
)
page
=
self
.
paginate_queryset
(
queryset
)
serializer
=
self
.
get_serializer
(
page
,
many
=
True
)
serializer
=
self
.
get_pagination_serializer
(
page
)
serializer
.
context
.
update
({
'sort_order'
:
order_by_input
})
# pylint: disable=maybe-no-member
response
=
self
.
get_paginated_response
(
serializer
.
data
)
return
Response
(
serializer
.
data
)
# pylint: disable=maybe-no-member
if
order_by_input
is
not
None
:
response
.
data
[
'sort_order'
]
=
order_by_input
return
response
def
post
(
self
,
request
):
def
post
(
self
,
request
):
"""POST /api/team/v0/teams/"""
"""POST /api/team/v0/teams/"""
...
@@ -544,16 +473,14 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
...
@@ -544,16 +473,14 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
'add_method'
:
'added_on_create'
'add_method'
:
'added_on_create'
}
}
)
)
return
Response
(
CourseTeamSerializer
(
team
)
.
data
)
data
=
CourseTeamSerializer
(
team
,
context
=
{
"request"
:
request
})
.
data
return
Response
(
data
)
def
get_page
(
self
):
def
get_page
(
self
):
""" Returns page number specified in args, params, or defaults to 1. """
""" Returns page number specified in args, params, or defaults to 1. """
# This code is taken from within the GenericAPIView#paginate_queryset method.
# This code is taken from within the GenericAPIView#paginate_queryset method.
# We need need access to the page outside of that method for our paginate_search_results method
# We need need access to the page outside of that method for our paginate_search_results method
page_kwarg
=
self
.
kwargs
.
get
(
self
.
pag
inator
.
page_query_param
)
page_kwarg
=
self
.
kwargs
.
get
(
self
.
pag
e_kwarg
)
page_query_param
=
self
.
request
.
QUERY_PARAMS
.
get
(
self
.
pag
inator
.
page_query_param
)
page_query_param
=
self
.
request
.
QUERY_PARAMS
.
get
(
self
.
pag
e_kwarg
)
return
page_kwarg
or
page_query_param
or
1
return
page_kwarg
or
page_query_param
or
1
...
@@ -765,7 +692,9 @@ class TopicListView(GenericAPIView):
...
@@ -765,7 +692,9 @@ class TopicListView(GenericAPIView):
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
pagination_class
=
TopicsPagination
paginate_by
=
TOPICS_PER_PAGE
paginate_by_param
=
'page_size'
def
get
(
self
,
request
):
def
get
(
self
,
request
):
"""GET /api/team/v0/topics/?course_id={course_id}"""
"""GET /api/team/v0/topics/?course_id={course_id}"""
...
@@ -811,20 +740,18 @@ class TopicListView(GenericAPIView):
...
@@ -811,20 +740,18 @@ class TopicListView(GenericAPIView):
add_team_count
(
topics
,
course_id
)
add_team_count
(
topics
,
course_id
)
topics
.
sort
(
key
=
lambda
t
:
t
[
'team_count'
],
reverse
=
True
)
topics
.
sort
(
key
=
lambda
t
:
t
[
'team_count'
],
reverse
=
True
)
page
=
self
.
paginate_queryset
(
topics
)
page
=
self
.
paginate_queryset
(
topics
)
serializer
=
TopicSerializer
(
# Since team_count has already been added to all the topics, use PaginatedTopicSerializer.
page
,
# Even though sorting is done outside of the serializer, sort_order needs to be passed
context
=
{
'course_id'
:
course_id
},
# to the serializer so that the paginated results indicate how they were sorted.
many
=
True
,
serializer
=
PaginatedTopicSerializer
(
page
,
context
=
{
'course_id'
:
course_id
,
'sort_order'
:
ordering
})
)
else
:
else
:
page
=
self
.
paginate_queryset
(
topics
)
page
=
self
.
paginate_queryset
(
topics
)
# Use the serializer that adds team_count in a bulk operation per page.
# Use the serializer that adds team_count in a bulk operation per page.
serializer
=
BulkTeamCountTopicSerializer
(
page
,
context
=
{
'course_id'
:
course_id
},
many
=
True
)
serializer
=
BulkTeamCountPaginatedTopicSerializer
(
page
,
context
=
{
'course_id'
:
course_id
,
'sort_order'
:
ordering
}
response
=
self
.
get_paginated_response
(
serializer
.
data
)
)
response
.
data
[
'sort_order'
]
=
ordering
return
response
return
Response
(
serializer
.
data
)
def
get_alphabetical_topics
(
course_module
):
def
get_alphabetical_topics
(
course_module
):
...
@@ -1033,8 +960,13 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
...
@@ -1033,8 +960,13 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
serializer_class
=
MembershipSerializer
serializer_class
=
MembershipSerializer
paginate_by
=
10
paginate_by_param
=
'page_size'
pagination_serializer_class
=
PaginationSerializer
def
get
(
self
,
request
):
def
get
(
self
,
request
):
"""GET /api/team/v0/team_membership"""
"""GET /api/team/v0/team_membership"""
specified_username_or_team
=
False
specified_username_or_team
=
False
...
@@ -1091,8 +1023,8 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
...
@@ -1091,8 +1023,8 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
queryset
=
CourseTeamMembership
.
get_memberships
(
username
,
course_keys
,
team_id
)
queryset
=
CourseTeamMembership
.
get_memberships
(
username
,
course_keys
,
team_id
)
page
=
self
.
paginate_queryset
(
queryset
)
page
=
self
.
paginate_queryset
(
queryset
)
serializer
=
self
.
get_
serializer
(
page
,
many
=
Tru
e
)
serializer
=
self
.
get_
pagination_serializer
(
pag
e
)
return
self
.
get_paginated_response
(
serializer
.
data
)
return
Response
(
serializer
.
data
)
# pylint: disable=maybe-no-member
def
post
(
self
,
request
):
def
post
(
self
,
request
):
"""POST /api/team/v0/team_membership"""
"""POST /api/team/v0/team_membership"""
...
...
lms/envs/common.py
View file @
2fd6add5
...
@@ -1866,12 +1866,6 @@ INSTALLED_APPS = (
...
@@ -1866,12 +1866,6 @@ INSTALLED_APPS = (
'provider.oauth2'
,
'provider.oauth2'
,
'oauth2_provider'
,
'oauth2_provider'
,
# We don't use this directly (since we use OAuth2), but we need to install it anyway.
# When a user is deleted, Django queries all tables with a FK to the auth_user table,
# and since django-rest-framework-oauth imports this, it will try to access tables
# defined by oauth_provider. If those tables don't exist, an error can occur.
'oauth_provider'
,
'auth_exchange'
,
'auth_exchange'
,
# For the wiki
# For the wiki
...
@@ -1987,14 +1981,6 @@ INSTALLED_APPS = (
...
@@ -1987,14 +1981,6 @@ INSTALLED_APPS = (
CSRF_COOKIE_AGE
=
60
*
60
*
24
*
7
*
52
CSRF_COOKIE_AGE
=
60
*
60
*
24
*
7
*
52
######################### Django Rest Framework ########################
REST_FRAMEWORK
=
{
'DEFAULT_PAGINATION_CLASS'
:
'openedx.core.lib.api.paginators.DefaultPagination'
,
'PAGE_SIZE'
:
10
,
}
######################### MARKETING SITE ###############################
######################### MARKETING SITE ###############################
EDXMKTG_LOGGED_IN_COOKIE_NAME
=
'edxloggedin'
EDXMKTG_LOGGED_IN_COOKIE_NAME
=
'edxloggedin'
EDXMKTG_USER_INFO_COOKIE_NAME
=
'edx-user-info'
EDXMKTG_USER_INFO_COOKIE_NAME
=
'edx-user-info'
...
...
lms/startup.py
View file @
2fd6add5
...
@@ -9,12 +9,16 @@ from django.conf import settings
...
@@ -9,12 +9,16 @@ from django.conf import settings
# Force settings to run so that the python path is modified
# Force settings to run so that the python path is modified
settings
.
INSTALLED_APPS
# pylint: disable=pointless-statement
settings
.
INSTALLED_APPS
# pylint: disable=pointless-statement
from
instructor.services
import
InstructorService
from
openedx.core.lib.django_startup
import
autostartup
from
openedx.core.lib.django_startup
import
autostartup
import
edxmako
import
edxmako
import
logging
import
logging
from
monkey_patch
import
django_utils_translation
from
monkey_patch
import
django_utils_translation
import
analytics
import
analytics
from
edx_proctoring.runtime
import
set_runtime_service
from
openedx.core.djangoapps.credit.services
import
CreditService
log
=
logging
.
getLogger
(
__name__
)
log
=
logging
.
getLogger
(
__name__
)
...
@@ -47,11 +51,6 @@ def run():
...
@@ -47,11 +51,6 @@ def run():
# right now edx_proctoring is dependent on the openedx.core.djangoapps.credit
# right now edx_proctoring is dependent on the openedx.core.djangoapps.credit
# as well as the instructor dashboard (for deleting student attempts)
# as well as the instructor dashboard (for deleting student attempts)
if
settings
.
FEATURES
.
get
(
'ENABLE_PROCTORED_EXAMS'
):
if
settings
.
FEATURES
.
get
(
'ENABLE_PROCTORED_EXAMS'
):
# Import these here to avoid circular dependencies of the form:
# edx-platform app --> DRF --> django translation --> edx-platform app
from
edx_proctoring.runtime
import
set_runtime_service
from
instructor.services
import
InstructorService
from
openedx.core.djangoapps.credit.services
import
CreditService
set_runtime_service
(
'credit'
,
CreditService
())
set_runtime_service
(
'credit'
,
CreditService
())
set_runtime_service
(
'instructor'
,
InstructorService
())
set_runtime_service
(
'instructor'
,
InstructorService
())
...
...
openedx/core/djangoapps/content/course_structures/api/v0/api.py
View file @
2fd6add5
...
@@ -124,4 +124,4 @@ def course_grading_policy(course_key):
...
@@ -124,4 +124,4 @@ def course_grading_policy(course_key):
final grade.
final grade.
"""
"""
course
=
_retrieve_course
(
course_key
)
course
=
_retrieve_course
(
course_key
)
return
GradingPolicySerializer
(
course
.
raw_grader
,
many
=
True
)
.
data
return
GradingPolicySerializer
(
course
.
raw_grader
)
.
data
openedx/core/djangoapps/content/course_structures/api/v0/serializers.py
View file @
2fd6add5
"""
"""
API Serializers
API Serializers
"""
"""
from
collections
import
defaultdict
from
rest_framework
import
serializers
from
rest_framework
import
serializers
...
@@ -13,58 +11,23 @@ class GradingPolicySerializer(serializers.Serializer):
...
@@ -13,58 +11,23 @@ class GradingPolicySerializer(serializers.Serializer):
dropped
=
serializers
.
IntegerField
(
source
=
'drop_count'
)
dropped
=
serializers
.
IntegerField
(
source
=
'drop_count'
)
weight
=
serializers
.
FloatField
()
weight
=
serializers
.
FloatField
()
def
to_representation
(
self
,
obj
):
"""
Return a representation of the grading policy.
"""
# Backwards compatibility with the behavior of DRF v2.
# When the grader dictionary was missing keys, DRF v2 would default to None;
# DRF v3 unhelpfully raises an exception.
return
dict
(
super
(
GradingPolicySerializer
,
self
)
.
to_representation
(
defaultdict
(
lambda
:
None
,
obj
)
)
)
# pylint: disable=invalid-name
# pylint: disable=invalid-name
class
BlockSerializer
(
serializers
.
Serializer
):
class
BlockSerializer
(
serializers
.
Serializer
):
""" Serializer for course structure block. """
""" Serializer for course structure block. """
id
=
serializers
.
CharField
(
source
=
'usage_key'
)
id
=
serializers
.
CharField
(
source
=
'usage_key'
)
type
=
serializers
.
CharField
(
source
=
'block_type'
)
type
=
serializers
.
CharField
(
source
=
'block_type'
)
parent
=
serializers
.
CharField
(
required
=
False
)
parent
=
serializers
.
CharField
(
source
=
'parent'
)
display_name
=
serializers
.
CharField
()
display_name
=
serializers
.
CharField
()
graded
=
serializers
.
BooleanField
(
default
=
False
)
graded
=
serializers
.
BooleanField
(
default
=
False
)
format
=
serializers
.
CharField
()
format
=
serializers
.
CharField
()
children
=
serializers
.
CharField
()
children
=
serializers
.
CharField
()
def
to_representation
(
self
,
obj
):
"""
Return a representation of the block.
NOTE: this method maintains backwards compatibility with the behavior
of Django Rest Framework v2.
"""
data
=
super
(
BlockSerializer
,
self
)
.
to_representation
(
obj
)
# Backwards compatibility with the behavior of DRF v2
# Include a NULL value for "parent" in the representation
# (instead of excluding the key entirely)
if
obj
.
get
(
"parent"
)
is
None
:
data
[
"parent"
]
=
None
# Backwards compatibility with the behavior of DRF v2
# Leave the children list as a list instead of serializing
# it to a string.
data
[
"children"
]
=
obj
[
"children"
]
return
data
class
CourseStructureSerializer
(
serializers
.
Serializer
):
class
CourseStructureSerializer
(
serializers
.
Serializer
):
""" Serializer for course structure. """
""" Serializer for course structure. """
root
=
serializers
.
CharField
()
root
=
serializers
.
CharField
(
source
=
'root'
)
blocks
=
serializers
.
SerializerMethodField
()
blocks
=
serializers
.
SerializerMethodField
(
'get_blocks'
)
def
get_blocks
(
self
,
structure
):
def
get_blocks
(
self
,
structure
):
""" Serialize the individual blocks. """
""" Serialize the individual blocks. """
...
...
openedx/core/djangoapps/credit/serializers.py
View file @
2fd6add5
...
@@ -2,33 +2,12 @@
...
@@ -2,33 +2,12 @@
from
rest_framework
import
serializers
from
rest_framework
import
serializers
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys
import
InvalidKeyError
from
openedx.core.djangoapps.credit.models
import
CreditCourse
from
openedx.core.djangoapps.credit.models
import
CreditCourse
class
CourseKeyField
(
serializers
.
Field
):
"""
Serializer field for a model CourseKey field.
"""
def
to_representation
(
self
,
data
):
"""Convert a course key to unicode. """
return
unicode
(
data
)
def
to_internal_value
(
self
,
data
):
"""Convert unicode to a course key. """
try
:
return
CourseKey
.
from_string
(
data
)
except
InvalidKeyError
as
ex
:
raise
serializers
.
ValidationError
(
"Invalid course key: {msg}"
.
format
(
msg
=
ex
.
msg
))
class
CreditCourseSerializer
(
serializers
.
ModelSerializer
):
class
CreditCourseSerializer
(
serializers
.
ModelSerializer
):
""" CreditCourse Serializer """
""" CreditCourse Serializer """
course_key
=
CourseKeyField
()
class
Meta
(
object
):
# pylint: disable=missing-docstring
class
Meta
(
object
):
# pylint: disable=missing-docstring
model
=
CreditCourse
model
=
CreditCourse
exclude
=
(
'id'
,)
exclude
=
(
'id'
,)
openedx/core/djangoapps/credit/tests/test_views.py
View file @
2fd6add5
...
@@ -393,7 +393,10 @@ class CreditCourseViewSetTests(TestCase):
...
@@ -393,7 +393,10 @@ class CreditCourseViewSetTests(TestCase):
# POSTs without a CSRF token should fail.
# POSTs without a CSRF token should fail.
response
=
client
.
post
(
self
.
path
,
data
=
json
.
dumps
(
data
),
content_type
=
JSON
)
response
=
client
.
post
(
self
.
path
,
data
=
json
.
dumps
(
data
),
content_type
=
JSON
)
self
.
assertEqual
(
response
.
status_code
,
403
)
# NOTE (CCB): Ordinarily we would expect a 403; however, since the CSRF validation and session authentication
# fail, DRF considers the request to be unauthenticated.
self
.
assertEqual
(
response
.
status_code
,
401
)
self
.
assertIn
(
'CSRF'
,
response
.
content
)
self
.
assertIn
(
'CSRF'
,
response
.
content
)
# Retrieve a CSRF token
# Retrieve a CSRF token
...
...
openedx/core/djangoapps/credit/views.py
View file @
2fd6add5
...
@@ -18,9 +18,8 @@ from django.views.decorators.http import require_POST, require_GET
...
@@ -18,9 +18,8 @@ from django.views.decorators.http import require_POST, require_GET
from
opaque_keys
import
InvalidKeyError
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
import
pytz
import
pytz
from
rest_framework
import
viewsets
,
mixins
,
permissions
from
rest_framework
import
viewsets
,
mixins
,
permissions
,
authentication
from
rest_framework.authentication
import
SessionAuthentication
from
rest_framework_oauth.authentication
import
OAuth2Authentication
from
util.json_request
import
JsonResponse
from
util.json_request
import
JsonResponse
from
util.date_utils
import
from_timestamp
from
util.date_utils
import
from_timestamp
from
openedx.core.djangoapps.credit
import
api
from
openedx.core.djangoapps.credit
import
api
...
@@ -378,28 +377,17 @@ class CreditCourseViewSet(mixins.CreateModelMixin, mixins.UpdateModelMixin, view
...
@@ -378,28 +377,17 @@ class CreditCourseViewSet(mixins.CreateModelMixin, mixins.UpdateModelMixin, view
lookup_value_regex
=
settings
.
COURSE_KEY_REGEX
lookup_value_regex
=
settings
.
COURSE_KEY_REGEX
queryset
=
CreditCourse
.
objects
.
all
()
queryset
=
CreditCourse
.
objects
.
all
()
serializer_class
=
CreditCourseSerializer
serializer_class
=
CreditCourseSerializer
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
,)
authentication_classes
=
(
authentication
.
OAuth2Authentication
,
authentication
.
SessionAuthentication
,)
permission_classes
=
(
permissions
.
IsAuthenticated
,
permissions
.
IsAdminUser
)
permission_classes
=
(
permissions
.
IsAuthenticated
,
permissions
.
IsAdminUser
)
# In Django Rest Framework v3, there is a default pagination
# class that transmutes the response data into a dictionary
# with pagination information. The original response data (a list)
# is stored in a "results" value of the dictionary.
# For backwards compatibility with the existing API, we disable
# the default behavior by setting the pagination_class to None.
pagination_class
=
None
# This CSRF exemption only applies when authenticating without SessionAuthentication.
# This CSRF exemption only applies when authenticating without SessionAuthentication.
# SessionAuthentication will enforce CSRF protection.
# SessionAuthentication will enforce CSRF protection.
@method_decorator
(
csrf_exempt
)
@method_decorator
(
csrf_exempt
)
def
dispatch
(
self
,
request
,
*
args
,
**
kwargs
):
def
dispatch
(
self
,
request
,
*
args
,
**
kwargs
):
return
super
(
CreditCourseViewSet
,
self
)
.
dispatch
(
request
,
*
args
,
**
kwargs
)
# Convert the course ID/key from a string to an actual CourseKey object.
course_id
=
kwargs
.
get
(
self
.
lookup_field
,
None
)
def
get_object
(
self
):
if
course_id
:
# Convert the serialized course key into a CourseKey instance
kwargs
[
self
.
lookup_field
]
=
CourseKey
.
from_string
(
course_id
)
# so we can look up the object.
course_key
=
self
.
kwargs
.
get
(
self
.
lookup_field
)
if
course_key
is
not
None
:
self
.
kwargs
[
self
.
lookup_field
]
=
CourseKey
.
from_string
(
course_key
)
return
super
(
CreditCourseViewSet
,
self
)
.
get_object
(
)
return
super
(
CreditCourseViewSet
,
self
)
.
dispatch
(
request
,
*
args
,
**
kwargs
)
openedx/core/djangoapps/profile_images/tests/test_views.py
View file @
2fd6add5
...
@@ -30,40 +30,6 @@ TEST_UPLOAD_DT = datetime.datetime(2002, 1, 9, 15, 43, 01, tzinfo=UTC)
...
@@ -30,40 +30,6 @@ TEST_UPLOAD_DT = datetime.datetime(2002, 1, 9, 15, 43, 01, tzinfo=UTC)
TEST_UPLOAD_DT2
=
datetime
.
datetime
(
2003
,
1
,
9
,
15
,
43
,
01
,
tzinfo
=
UTC
)
TEST_UPLOAD_DT2
=
datetime
.
datetime
(
2003
,
1
,
9
,
15
,
43
,
01
,
tzinfo
=
UTC
)
class
PatchedClient
(
APIClient
):
"""
Patch DRF's APIClient to avoid a unicode error on file upload.
Famous last words: This is a *temporary* fix that we should be
able to remove once we upgrade Django past 1.4.
"""
def
request
(
self
,
*
args
,
**
kwargs
):
"""Construct an API request. """
# DRF's default test client implementation uses `six.text_type()`
# to convert the CONTENT_TYPE to `unicode`. In Django 1.4,
# this causes a `UnicodeDecodeError` when Django parses a multipart
# upload.
#
# This is the DRF code we're working around:
# https://github.com/tomchristie/django-rest-framework/blob/3.1.3/rest_framework/compat.py#L227
#
# ... and this is the Django code that raises the exception:
#
# https://github.com/django/django/blob/1.4.22/django/http/multipartparser.py#L435
#
# Django unhelpfully swallows the exception, so to the application code
# it appears as though the user didn't send any file data.
#
# This appears to be an issue only with requests constructed in the test
# suite, not with the upload code used in production.
#
if
isinstance
(
kwargs
.
get
(
"CONTENT_TYPE"
),
basestring
):
kwargs
[
"CONTENT_TYPE"
]
=
str
(
kwargs
[
"CONTENT_TYPE"
])
return
super
(
PatchedClient
,
self
)
.
request
(
*
args
,
**
kwargs
)
class
ProfileImageEndpointTestCase
(
UserSettingsEventTestMixin
,
APITestCase
):
class
ProfileImageEndpointTestCase
(
UserSettingsEventTestMixin
,
APITestCase
):
"""
"""
Base class / shared infrastructure for tests of profile_image "upload" and
Base class / shared infrastructure for tests of profile_image "upload" and
...
@@ -145,10 +111,6 @@ class ProfileImageUploadTestCase(ProfileImageEndpointTestCase):
...
@@ -145,10 +111,6 @@ class ProfileImageUploadTestCase(ProfileImageEndpointTestCase):
"""
"""
_view_name
=
"profile_image_upload"
_view_name
=
"profile_image_upload"
# Use the patched version of the API client to workaround a unicode issue
# with DRF 3.1 and Django 1.4. Remove this after we upgrade Django past 1.4!
client_class
=
PatchedClient
def
check_upload_event_emitted
(
self
,
old
=
None
,
new
=
TEST_UPLOAD_DT
):
def
check_upload_event_emitted
(
self
,
old
=
None
,
new
=
TEST_UPLOAD_DT
):
"""
"""
Make sure we emit a UserProfile event corresponding to the
Make sure we emit a UserProfile event corresponding to the
...
...
openedx/core/djangoapps/user_api/accounts/api.py
View file @
2fd6add5
...
@@ -183,7 +183,7 @@ def update_account_settings(requesting_user, update, username=None):
...
@@ -183,7 +183,7 @@ def update_account_settings(requesting_user, update, username=None):
serializer
.
save
()
serializer
.
save
()
if
"language_proficiencies"
in
update
:
if
"language_proficiencies"
in
update
:
new_language_proficiencies
=
update
[
"language_proficiencies"
]
new_language_proficiencies
=
legacy_profile_serializer
.
data
[
"language_proficiencies"
]
emit_setting_changed_event
(
emit_setting_changed_event
(
user
=
existing_user
,
user
=
existing_user
,
db_table
=
existing_user_profile
.
language_proficiencies
.
model
.
_meta
.
db_table
,
db_table
=
existing_user_profile
.
language_proficiencies
.
model
.
_meta
.
db_table
,
...
...
openedx/core/djangoapps/user_api/accounts/serializers.py
View file @
2fd6add5
...
@@ -53,7 +53,7 @@ class UserReadOnlySerializer(serializers.Serializer):
...
@@ -53,7 +53,7 @@ class UserReadOnlySerializer(serializers.Serializer):
super
(
UserReadOnlySerializer
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
super
(
UserReadOnlySerializer
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
def
to_
representation
(
self
,
user
):
def
to_
native
(
self
,
user
):
"""
"""
Overwrite to_native to handle custom logic since we are serializing two models as one here
Overwrite to_native to handle custom logic since we are serializing two models as one here
:param user: User object
:param user: User object
...
@@ -152,8 +152,8 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea
...
@@ -152,8 +152,8 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea
Class that serializes the portion of UserProfile model needed for account information.
Class that serializes the portion of UserProfile model needed for account information.
"""
"""
profile_image
=
serializers
.
SerializerMethodField
(
"_get_profile_image"
)
profile_image
=
serializers
.
SerializerMethodField
(
"_get_profile_image"
)
requires_parental_consent
=
serializers
.
SerializerMethodField
()
requires_parental_consent
=
serializers
.
SerializerMethodField
(
"get_requires_parental_consent"
)
language_proficiencies
=
LanguageProficiencySerializer
(
many
=
True
,
required
=
False
)
language_proficiencies
=
LanguageProficiencySerializer
(
many
=
True
,
allow_add_remove
=
True
,
required
=
False
)
class
Meta
(
object
):
# pylint: disable=missing-docstring
class
Meta
(
object
):
# pylint: disable=missing-docstring
model
=
UserProfile
model
=
UserProfile
...
@@ -165,21 +165,25 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea
...
@@ -165,21 +165,25 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea
read_only_fields
=
()
read_only_fields
=
()
explicit_read_only_fields
=
(
"profile_image"
,
"requires_parental_consent"
)
explicit_read_only_fields
=
(
"profile_image"
,
"requires_parental_consent"
)
def
validate_name
(
self
,
new_nam
e
):
def
validate_name
(
self
,
attrs
,
sourc
e
):
""" Enforce minimum length for name. """
""" Enforce minimum length for name. """
if
len
(
new_name
)
<
NAME_MIN_LENGTH
:
if
source
in
attrs
:
raise
serializers
.
ValidationError
(
new_name
=
attrs
[
source
]
.
strip
()
"The name field must be at least {} characters long."
.
format
(
NAME_MIN_LENGTH
)
if
len
(
new_name
)
<
NAME_MIN_LENGTH
:
)
raise
serializers
.
ValidationError
(
return
new_name
"The name field must be at least {} characters long."
.
format
(
NAME_MIN_LENGTH
)
)
attrs
[
source
]
=
new_name
def
validate_language_proficiencies
(
self
,
value
):
return
attrs
def
validate_language_proficiencies
(
self
,
attrs
,
source
):
""" Enforce all languages are unique. """
""" Enforce all languages are unique. """
language_proficiencies
=
[
language
for
language
in
value
]
language_proficiencies
=
[
language
for
language
in
attrs
.
get
(
source
,
[])
]
unique_language_proficiencies
=
set
(
language
[
"code"
]
for
language
in
language_proficiencies
)
unique_language_proficiencies
=
set
(
language
.
code
for
language
in
language_proficiencies
)
if
len
(
language_proficiencies
)
!=
len
(
unique_language_proficiencies
):
if
len
(
language_proficiencies
)
!=
len
(
unique_language_proficiencies
):
raise
serializers
.
ValidationError
(
"The language_proficiencies field must consist of unique languages"
)
raise
serializers
.
ValidationError
(
"The language_proficiencies field must consist of unique languages"
)
return
value
return
attrs
def
transform_gender
(
self
,
user_profile
,
value
):
def
transform_gender
(
self
,
user_profile
,
value
):
""" Converts empty string to None, to indicate not set. Replaced by to_representation in version 3. """
""" Converts empty string to None, to indicate not set. Replaced by to_representation in version 3. """
...
@@ -226,29 +230,3 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea
...
@@ -226,29 +230,3 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea
call the method with a single argument, the user_profile object.
call the method with a single argument, the user_profile object.
"""
"""
return
AccountLegacyProfileSerializer
.
get_profile_image
(
user_profile
,
user_profile
.
user
)
return
AccountLegacyProfileSerializer
.
get_profile_image
(
user_profile
,
user_profile
.
user
)
def
update
(
self
,
instance
,
validated_data
):
"""
Update the profile, including nested fields.
"""
language_proficiencies
=
validated_data
.
pop
(
"language_proficiencies"
,
None
)
# Update all fields on the user profile that are writeable,
# except for "language_proficiencies", which we'll update separately
update_fields
=
set
(
self
.
get_writeable_fields
())
-
set
([
"language_proficiencies"
])
for
field_name
in
update_fields
:
default
=
getattr
(
instance
,
field_name
)
field_value
=
validated_data
.
get
(
field_name
,
default
)
setattr
(
instance
,
field_name
,
field_value
)
instance
.
save
()
# Now update the related language proficiency
if
language_proficiencies
is
not
None
:
instance
.
language_proficiencies
.
all
()
.
delete
()
instance
.
language_proficiencies
.
bulk_create
([
LanguageProficiency
(
user_profile
=
instance
,
code
=
language
[
"code"
])
for
language
in
language_proficiencies
])
return
instance
openedx/core/djangoapps/user_api/accounts/tests/test_api.py
View file @
2fd6add5
...
@@ -164,10 +164,7 @@ class TestAccountApi(UserSettingsEventTestMixin, TestCase):
...
@@ -164,10 +164,7 @@ class TestAccountApi(UserSettingsEventTestMixin, TestCase):
field_errors
=
context_manager
.
exception
.
field_errors
field_errors
=
context_manager
.
exception
.
field_errors
self
.
assertEqual
(
3
,
len
(
field_errors
))
self
.
assertEqual
(
3
,
len
(
field_errors
))
self
.
assertEqual
(
"This field is not editable via this API"
,
field_errors
[
"username"
][
"developer_message"
])
self
.
assertEqual
(
"This field is not editable via this API"
,
field_errors
[
"username"
][
"developer_message"
])
self
.
assertIn
(
self
.
assertIn
(
"Select a valid choice"
,
field_errors
[
"gender"
][
"developer_message"
])
"Value
\'
undecided
\'
is not valid for field
\'
gender
\'
"
,
field_errors
[
"gender"
][
"developer_message"
]
)
self
.
assertIn
(
"Valid e-mail address required."
,
field_errors
[
"email"
][
"developer_message"
])
self
.
assertIn
(
"Valid e-mail address required."
,
field_errors
[
"email"
][
"developer_message"
])
@patch
(
'django.core.mail.send_mail'
)
@patch
(
'django.core.mail.send_mail'
)
...
...
openedx/core/djangoapps/user_api/accounts/tests/test_views.py
View file @
2fd6add5
...
@@ -359,19 +359,16 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -359,19 +359,16 @@ class TestAccountAPI(UserAPITestCase):
self
.
assertEqual
(
404
,
response
.
status_code
)
self
.
assertEqual
(
404
,
response
.
status_code
)
@ddt.data
(
@ddt.data
(
(
"gender"
,
"f"
,
"not a gender"
,
u
'"not a gender" is not a valid choice.'
),
(
"gender"
,
"f"
,
"not a gender"
,
u
"Select a valid choice. not a gender is not one of the available choices."
),
(
"level_of_education"
,
"none"
,
u"ȻħȺɍłɇs"
,
u
'"ȻħȺɍłɇs" is not a valid choice.'
),
(
"level_of_education"
,
"none"
,
u"ȻħȺɍłɇs"
,
u
"Select a valid choice. ȻħȺɍłɇs is not one of the available choices."
),
(
"country"
,
"GB"
,
"XY"
,
u
'"XY" is not a valid choice.'
),
(
"country"
,
"GB"
,
"XY"
,
u
"Select a valid choice. XY is not one of the available choices."
),
(
"year_of_birth"
,
2009
,
"not_an_int"
,
u"
A valid integer is required
."
),
(
"year_of_birth"
,
2009
,
"not_an_int"
,
u"
Enter a whole number
."
),
(
"name"
,
"bob"
,
"z"
*
256
,
u"Ensure this
field has no more than 255 characters
."
),
(
"name"
,
"bob"
,
"z"
*
256
,
u"Ensure this
value has at most 255 characters (it has 256)
."
),
(
"name"
,
u"ȻħȺɍłɇs"
,
"z "
,
u"The name field must be at least 2 characters long."
),
(
"name"
,
u"ȻħȺɍłɇs"
,
"z "
,
u"The name field must be at least 2 characters long."
),
(
"goals"
,
"Smell the roses"
),
(
"goals"
,
"Smell the roses"
),
(
"mailing_address"
,
"Sesame Street"
),
(
"mailing_address"
,
"Sesame Street"
),
# Note that we store the raw data, so it is up to client to escape the HTML.
# Note that we store the raw data, so it is up to client to escape the HTML.
(
(
"bio"
,
u"<html>Lacrosse-playing superhero 壓是進界推日不復女</html>"
,
"z"
*
3001
,
u"Ensure this value has at most 3000 characters (it has 3001)."
),
"bio"
,
u"<html>Lacrosse-playing superhero 壓是進界推日不復女</html>"
,
"z"
*
3001
,
u"Ensure this field has no more than 3000 characters."
),
# Note that email is tested below, as it is not immediately updated.
# Note that email is tested below, as it is not immediately updated.
# Note that language_proficiencies is tested below as there are multiple error and success conditions.
# Note that language_proficiencies is tested below as there are multiple error and success conditions.
)
)
...
@@ -571,10 +568,10 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -571,10 +568,10 @@ class TestAccountAPI(UserAPITestCase):
self
.
assertItemsEqual
(
response
.
data
[
"language_proficiencies"
],
proficiencies
)
self
.
assertItemsEqual
(
response
.
data
[
"language_proficiencies"
],
proficiencies
)
@ddt.data
(
@ddt.data
(
(
u"not_a_list"
,
{
u'non_field_errors'
:
[
u'Expected a list of items but got type "unicode".'
]}
),
(
u"not_a_list"
,
[{
u'non_field_errors'
:
[
u'Expected a list of items.'
]}]
),
([
u"not_a_JSON_object"
],
[{
u'non_field_errors'
:
[
u'Invalid data
. Expected a dictionary, but got unicode.
'
]}]),
([
u"not_a_JSON_object"
],
[{
u'non_field_errors'
:
[
u'Invalid data'
]}]),
([{}],
[{
"code"
:
[
u"This field is required."
]}]),
([{}],
[{
"code"
:
[
u"This field is required."
]}]),
([{
u"code"
:
u"invalid_language_code"
}],
[{
'code'
:
[
u'
"invalid_language_code" is not a valid choice
.'
]}]),
([{
u"code"
:
u"invalid_language_code"
}],
[{
'code'
:
[
u'
Select a valid choice. invalid_language_code is not one of the available choices
.'
]}]),
([{
u"code"
:
u"kw"
},
{
u"code"
:
u"el"
},
{
u"code"
:
u"kw"
}],
[
u'The language_proficiencies field must consist of unique languages'
]),
([{
u"code"
:
u"kw"
},
{
u"code"
:
u"el"
},
{
u"code"
:
u"kw"
}],
[
u'The language_proficiencies field must consist of unique languages'
]),
)
)
@ddt.unpack
@ddt.unpack
...
...
openedx/core/djangoapps/user_api/preferences/api.py
View file @
2fd6add5
...
@@ -9,10 +9,9 @@ from django.conf import settings
...
@@ -9,10 +9,9 @@ from django.conf import settings
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.db
import
IntegrityError
from
django.db
import
IntegrityError
from
django.utils.translation
import
ugettext
as
_
from
django.utils.translation
import
ugettext
as
_
from
student.models
import
User
,
UserProfile
from
django.utils.translation
import
ugettext_noop
from
django.utils.translation
import
ugettext_noop
from
student.models
import
User
,
UserProfile
from
request_cache
import
get_request_or_stub
from
..errors
import
(
from
..errors
import
(
UserAPIInternalError
,
UserAPIRequestError
,
UserNotFound
,
UserNotAuthorized
,
UserAPIInternalError
,
UserAPIRequestError
,
UserNotFound
,
UserNotAuthorized
,
PreferenceValidationError
,
PreferenceUpdateError
PreferenceValidationError
,
PreferenceUpdateError
...
@@ -69,17 +68,7 @@ def get_user_preferences(requesting_user, username=None):
...
@@ -69,17 +68,7 @@ def get_user_preferences(requesting_user, username=None):
UserAPIInternalError: the operation failed due to an unexpected error.
UserAPIInternalError: the operation failed due to an unexpected error.
"""
"""
existing_user
=
_get_user
(
requesting_user
,
username
,
allow_staff
=
True
)
existing_user
=
_get_user
(
requesting_user
,
username
,
allow_staff
=
True
)
user_serializer
=
UserSerializer
(
existing_user
)
# Django Rest Framework V3 uses the current request to version
# hyperlinked URLS, so we need to retrieve the request and pass
# it in the serializer's context (otherwise we get an AssertionError).
# We're retrieving the request from the cache rather than passing it in
# as an argument because this is an implementation detail of how we're
# serializing data, which we want to encapsulate in the API call.
context
=
{
"request"
:
get_request_or_stub
()
}
user_serializer
=
UserSerializer
(
existing_user
,
context
=
context
)
return
user_serializer
.
data
[
"preferences"
]
return
user_serializer
.
data
[
"preferences"
]
...
@@ -367,7 +356,7 @@ def validate_user_preference_serializer(serializer, preference_key, preference_v
...
@@ -367,7 +356,7 @@ def validate_user_preference_serializer(serializer, preference_key, preference_v
developer_message
=
u"Value '{preference_value}' not valid for preference '{preference_key}': {error}"
.
format
(
developer_message
=
u"Value '{preference_value}' not valid for preference '{preference_key}': {error}"
.
format
(
preference_key
=
preference_key
,
preference_value
=
preference_value
,
error
=
serializer
.
errors
preference_key
=
preference_key
,
preference_value
=
preference_value
,
error
=
serializer
.
errors
)
)
if
"key"
in
serializer
.
errors
:
if
serializer
.
errors
[
"key"
]
:
user_message
=
_
(
u"Invalid user preference key '{preference_key}'."
)
.
format
(
user_message
=
_
(
u"Invalid user preference key '{preference_key}'."
)
.
format
(
preference_key
=
preference_key
preference_key
=
preference_key
)
)
...
...
openedx/core/djangoapps/user_api/preferences/tests/test_api.py
View file @
2fd6add5
...
@@ -403,7 +403,7 @@ def get_expected_validation_developer_message(preference_key, preference_value):
...
@@ -403,7 +403,7 @@ def get_expected_validation_developer_message(preference_key, preference_value):
preference_key
=
preference_key
,
preference_key
=
preference_key
,
preference_value
=
preference_value
,
preference_value
=
preference_value
,
error
=
{
error
=
{
"key"
:
[
u"Ensure this
field has no more than 255 characters
."
]
"key"
:
[
u"Ensure this
value has at most 255 characters (it has 256)
."
]
}
}
)
)
...
...
openedx/core/djangoapps/user_api/serializers.py
View file @
2fd6add5
...
@@ -6,8 +6,8 @@ from .models import UserPreference
...
@@ -6,8 +6,8 @@ from .models import UserPreference
class
UserSerializer
(
serializers
.
HyperlinkedModelSerializer
):
class
UserSerializer
(
serializers
.
HyperlinkedModelSerializer
):
name
=
serializers
.
SerializerMethodField
()
name
=
serializers
.
SerializerMethodField
(
"get_name"
)
preferences
=
serializers
.
SerializerMethodField
()
preferences
=
serializers
.
SerializerMethodField
(
"get_preferences"
)
def
get_name
(
self
,
user
):
def
get_name
(
self
,
user
):
profile
=
UserProfile
.
objects
.
get
(
user
=
user
)
profile
=
UserProfile
.
objects
.
get
(
user
=
user
)
...
@@ -32,10 +32,9 @@ class UserPreferenceSerializer(serializers.HyperlinkedModelSerializer):
...
@@ -32,10 +32,9 @@ class UserPreferenceSerializer(serializers.HyperlinkedModelSerializer):
class
RawUserPreferenceSerializer
(
serializers
.
ModelSerializer
):
class
RawUserPreferenceSerializer
(
serializers
.
ModelSerializer
):
"""Serializer that generates a raw representation of a user preference.
"""
"""
Serializer that generates a raw representation of a user preference.
user
=
serializers
.
PrimaryKeyRelatedField
()
"""
user
=
serializers
.
PrimaryKeyRelatedField
(
queryset
=
User
.
objects
.
all
())
class
Meta
(
object
):
# pylint: disable=missing-docstring
class
Meta
(
object
):
# pylint: disable=missing-docstring
model
=
UserPreference
model
=
UserPreference
...
@@ -58,11 +57,3 @@ class ReadOnlyFieldsSerializerMixin(object):
...
@@ -58,11 +57,3 @@ class ReadOnlyFieldsSerializerMixin(object):
cls.Meta.read_only_fields tuple.
cls.Meta.read_only_fields tuple.
"""
"""
return
getattr
(
cls
.
Meta
,
'read_only_fields'
,
''
)
+
getattr
(
cls
.
Meta
,
'explicit_read_only_fields'
,
''
)
return
getattr
(
cls
.
Meta
,
'read_only_fields'
,
''
)
+
getattr
(
cls
.
Meta
,
'explicit_read_only_fields'
,
''
)
@classmethod
def
get_writeable_fields
(
cls
):
"""
Return all fields on this serializer that are writeable.
"""
all_fields
=
getattr
(
cls
.
Meta
,
'fields'
,
tuple
())
return
tuple
(
set
(
all_fields
)
-
set
(
cls
.
get_read_only_fields
()))
openedx/core/lib/api/authentication.py
View file @
2fd6add5
""" Common Authentication Handlers used across projects. """
""" Common Authentication Handlers used across projects. """
from
rest_framework.authentication
import
SessionAuthentication
from
rest_framework
import
authentication
from
rest_framework_oauth.authentication
import
OAuth2Authentication
from
rest_framework.exceptions
import
AuthenticationFailed
from
rest_framework.exceptions
import
AuthenticationFailed
from
rest_framework
_oauth
.compat
import
oauth2_provider
,
provider_now
from
rest_framework.compat
import
oauth2_provider
,
provider_now
class
SessionAuthenticationAllowInactiveUser
(
SessionAuthentication
):
class
SessionAuthenticationAllowInactiveUser
(
authentication
.
SessionAuthentication
):
"""Ensure that the user is logged in, but do not require the account to be active.
"""Ensure that the user is logged in, but do not require the account to be active.
We use this in the special case that a user has created an account,
We use this in the special case that a user has created an account,
...
@@ -52,7 +51,7 @@ class SessionAuthenticationAllowInactiveUser(SessionAuthentication):
...
@@ -52,7 +51,7 @@ class SessionAuthenticationAllowInactiveUser(SessionAuthentication):
return
(
user
,
None
)
return
(
user
,
None
)
class
OAuth2AuthenticationAllowInactiveUser
(
OAuth2Authentication
):
class
OAuth2AuthenticationAllowInactiveUser
(
authentication
.
OAuth2Authentication
):
"""
"""
This is a temporary workaround while the is_active field on the user is coupled
This is a temporary workaround while the is_active field on the user is coupled
with whether or not the user has verified ownership of their claimed email address.
with whether or not the user has verified ownership of their claimed email address.
...
...
openedx/core/lib/api/fields.py
View file @
2fd6add5
"""Fields useful for edX API implementations."""
"""Fields useful for edX API implementations."""
from
rest_framework.serializers
import
Field
from
django.core.exceptions
import
ValidationError
from
rest_framework.serializers
import
CharField
,
Field
class
ExpandableField
(
Field
):
class
ExpandableField
(
Field
):
...
@@ -16,19 +18,25 @@ class ExpandableField(Field):
...
@@ -16,19 +18,25 @@ class ExpandableField(Field):
self
.
expanded
=
kwargs
.
pop
(
'expanded_serializer'
)
self
.
expanded
=
kwargs
.
pop
(
'expanded_serializer'
)
super
(
ExpandableField
,
self
)
.
__init__
(
**
kwargs
)
super
(
ExpandableField
,
self
)
.
__init__
(
**
kwargs
)
def
to_representation
(
self
,
obj
):
def
field_to_native
(
self
,
obj
,
field_name
):
"""
"""Converts obj to a native representation, using the expanded serializer if the context requires it."""
Return a representation of the field that is either expanded or collapsed.
if
'expand'
in
self
.
context
and
field_name
in
self
.
context
[
'expand'
]:
"""
self
.
expanded
.
initialize
(
self
,
field_name
)
should_expand
=
self
.
field_name
in
self
.
context
.
get
(
"expand"
,
[])
return
self
.
expanded
.
field_to_native
(
obj
,
field_name
)
field
=
self
.
expanded
if
should_expand
else
self
.
collapsed
else
:
self
.
collapsed
.
initialize
(
self
,
field_name
)
return
self
.
collapsed
.
field_to_native
(
obj
,
field_name
)
# Avoid double-binding the field, otherwise we'll get
# an error about the source kwarg being redundant.
if
field
.
source
is
None
:
field
.
bind
(
self
.
field_name
,
self
)
if
should_expand
:
class
NonEmptyCharField
(
CharField
):
self
.
expanded
.
context
[
"expand"
]
=
set
(
field
.
context
.
get
(
"expand"
,
[]))
"""
A field that enforces non-emptiness even for partial updates.
return
field
.
to_representation
(
obj
)
This is necessary because prior to version 3, DRF skips validation for empty
values. Thus, CharField's min_length and RegexField cannot be used to
enforce this constraint.
"""
def
validate
(
self
,
value
):
super
(
NonEmptyCharField
,
self
)
.
validate
(
value
)
if
not
value
.
strip
():
raise
ValidationError
(
self
.
error_messages
[
"required"
])
openedx/core/lib/api/mixins.py
deleted
100644 → 0
View file @
00473d44
"""
Django Rest Framework view mixins.
"""
from
django.core.exceptions
import
ValidationError
from
django.http
import
Http404
from
rest_framework
import
status
from
rest_framework.mixins
import
CreateModelMixin
from
rest_framework.response
import
Response
class
PutAsCreateMixin
(
CreateModelMixin
):
"""
Backwards compatibility with Django Rest Framework v2, which allowed
creation of a new resource using PUT.
"""
def
update
(
self
,
request
,
*
args
,
**
kwargs
):
"""
Create/update course modes for a course.
"""
# First, try to update the existing instance
try
:
try
:
return
super
(
PutAsCreateMixin
,
self
)
.
update
(
request
,
*
args
,
**
kwargs
)
except
Http404
:
# If no instance exists yet, create it.
# This is backwards-compatible with the behavior of DRF v2.
return
super
(
PutAsCreateMixin
,
self
)
.
create
(
request
,
*
args
,
**
kwargs
)
# Backwards compatibility with DRF v2 behavior, which would catch model-level
# validation errors and return a 400
except
ValidationError
as
err
:
return
Response
(
err
.
messages
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
openedx/core/lib/api/paginators.py
View file @
2fd6add5
...
@@ -3,31 +3,6 @@
...
@@ -3,31 +3,6 @@
from
django.http
import
Http404
from
django.http
import
Http404
from
django.core.paginator
import
Paginator
,
InvalidPage
from
django.core.paginator
import
Paginator
,
InvalidPage
from
rest_framework.response
import
Response
from
rest_framework
import
pagination
class
DefaultPagination
(
pagination
.
PageNumberPagination
):
"""
Default paginator for APIs in edx-platform.
This is configured in settings to be automatically used
by any subclass of Django Rest Framework's generic API views.
"""
page_size_query_param
=
"page_size"
def
get_paginated_response
(
self
,
data
):
"""
Annotate the response with pagination information.
"""
return
Response
({
'next'
:
self
.
get_next_link
(),
'previous'
:
self
.
get_previous_link
(),
'count'
:
self
.
page
.
paginator
.
count
,
'num_pages'
:
self
.
page
.
paginator
.
num_pages
,
'results'
:
data
})
def
paginate_search_results
(
object_class
,
search_results
,
page_size
,
page
):
def
paginate_search_results
(
object_class
,
search_results
,
page_size
,
page
):
"""
"""
...
...
openedx/core/lib/api/serializers.py
View file @
2fd6add5
"""
from
rest_framework
import
pagination
,
serializers
Serializers to be used in APIs.
"""
from
rest_framework
import
serializers
class
PaginationSerializer
(
pagination
.
PaginationSerializer
):
"""
Custom PaginationSerializer for openedx.
Adds the following fields:
- num_pages: total number of pages
- current_page: the current page being returned
- start: the index of the first page item within the overall collection
"""
start_page
=
1
# django Paginator.page objects have 1-based indexes
num_pages
=
serializers
.
Field
(
source
=
'paginator.num_pages'
)
current_page
=
serializers
.
SerializerMethodField
(
'get_current_page'
)
start
=
serializers
.
SerializerMethodField
(
'get_start'
)
sort_order
=
serializers
.
SerializerMethodField
(
'get_sort_order'
)
def
get_current_page
(
self
,
page
):
"""Get the current page"""
return
page
.
number
def
get_start
(
self
,
page
):
"""Get the index of the first page item within the overall collection"""
return
(
self
.
get_current_page
(
page
)
-
self
.
start_page
)
*
page
.
paginator
.
per_page
def
get_sort_order
(
self
,
page
):
# pylint: disable=unused-argument
"""Get the order by which this collection was sorted"""
return
self
.
context
.
get
(
'sort_order'
)
class
CollapsedReferenceSerializer
(
serializers
.
HyperlinkedModelSerializer
):
class
CollapsedReferenceSerializer
(
serializers
.
HyperlinkedModelSerializer
):
...
@@ -30,10 +54,9 @@ class CollapsedReferenceSerializer(serializers.HyperlinkedModelSerializer):
...
@@ -30,10 +54,9 @@ class CollapsedReferenceSerializer(serializers.HyperlinkedModelSerializer):
super
(
CollapsedReferenceSerializer
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
super
(
CollapsedReferenceSerializer
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
fields
[
id_source
]
=
serializers
.
CharField
(
read_only
=
True
)
self
.
fields
[
id_source
]
=
serializers
.
CharField
(
read_only
=
True
,
source
=
id_source
)
self
.
fields
[
'url'
]
.
view_name
=
view_name
self
.
fields
[
'url'
]
.
view_name
=
view_name
self
.
fields
[
'url'
]
.
lookup_field
=
lookup_field
self
.
fields
[
'url'
]
.
lookup_field
=
lookup_field
self
.
fields
[
'url'
]
.
lookup_url_kwarg
=
lookup_field
class
Meta
(
object
):
class
Meta
(
object
):
"""Defines meta information for the ModelSerializer.
"""Defines meta information for the ModelSerializer.
...
...
openedx/core/lib/api/tests/test_authentication.py
View file @
2fd6add5
"""
"""Tests for util.authentication module."""
Tests for OAuth2. This module is copied from django-rest-framework-oauth (tests/test_authentication.py)
and updated to use our subclass of OAuth2Authentication.
"""
from
__future__
import
unicode_literals
import
datetime
from
django.conf.urls
import
patterns
,
url
,
include
from
django.contrib.auth.models
import
User
from
django.http
import
HttpResponse
from
django.test
import
TestCase
from
django.utils
import
unittest
from
django.utils.http
import
urlencode
from
rest_framework
import
status
from
rest_framework.permissions
import
IsAuthenticated
from
rest_framework_oauth
import
permissions
from
rest_framework_oauth.compat
import
oauth2_provider
,
oauth2_provider_scope
from
rest_framework.test
import
APIRequestFactory
,
APIClient
from
rest_framework.views
import
APIView
from
mock
import
patch
from
django.conf
import
settings
from
rest_framework
import
permissions
from
rest_framework.compat
import
patterns
,
url
from
rest_framework.tests
import
test_authentication
from
provider
import
scope
,
constants
from
provider
import
scope
,
constants
from
unittest
import
skipUnless
from
..authentication
import
OAuth2AuthenticationAllowInactiveUser
from
..authentication
import
OAuth2AuthenticationAllowInactiveUser
factory
=
APIRequestFactory
()
# pylint: disable=invalid-name
class
MockView
(
APIView
):
# pylint: disable=missing-docstring
permission_classes
=
(
IsAuthenticated
,)
def
get
(
self
,
request
):
# pylint: disable=missing-docstring,unused-argument
return
HttpResponse
({
'a'
:
1
,
'b'
:
2
,
'c'
:
3
})
def
post
(
self
,
request
):
# pylint: disable=missing-docstring,unused-argument
return
HttpResponse
({
'a'
:
1
,
'b'
:
2
,
'c'
:
3
})
def
put
(
self
,
request
):
# pylint: disable=missing-docstring,unused-argument
return
HttpResponse
({
'a'
:
1
,
'b'
:
2
,
'c'
:
3
})
# This is the a change we've made from the django-rest-framework-oauth version
class
OAuth2AuthAllowInactiveUserDebug
(
OAuth2AuthenticationAllowInactiveUser
):
# of these tests. We're subclassing our custom OAuth2AuthenticationAllowInactiveUser
"""
# instead of OAuth2Authentication.
A debug class analogous to the OAuth2AuthenticationDebug class that tests
class
OAuth2AuthenticationDebug
(
OAuth2AuthenticationAllowInactiveUser
):
# pylint: disable=missing-docstring
the OAuth2 flow with the access token sent in a query param."""
allow_query_params_token
=
True
allow_query_params_token
=
True
urlpatterns
=
patterns
(
# The following patch overrides the URL patterns for the MockView class used in
''
,
# rest_framework.tests.test_authentication so that the corresponding AllowInactiveUser
url
(
r'^oauth2/'
,
include
(
'provider.oauth2.urls'
,
namespace
=
'oauth2'
)),
# classes are tested instead.
url
(
r'^oauth2-test/$'
,
MockView
.
as_view
(
authentication_classes
=
[
OAuth2AuthenticationAllowInactiveUser
])),
@skipUnless
(
settings
.
FEATURES
.
get
(
'ENABLE_OAUTH2_PROVIDER'
),
'OAuth2 not enabled'
)
url
(
r'^oauth2-test-debug/$'
,
MockView
.
as_view
(
authentication_classes
=
[
OAuth2AuthenticationDebug
])),
@patch.object
(
url
(
test_authentication
,
r'^oauth2-with-scope-test/$'
,
'urlpatterns'
,
MockView
.
as_view
(
patterns
(
authentication_classes
=
[
OAuth2AuthenticationAllowInactiveUser
],
''
,
permission_classes
=
[
permissions
.
TokenHasReadWriteScope
]
url
(
r'^oauth2-test/$'
,
test_authentication
.
MockView
.
as_view
(
authentication_classes
=
[
OAuth2AuthenticationAllowInactiveUser
])
),
url
(
r'^oauth2-test-debug/$'
,
test_authentication
.
MockView
.
as_view
(
authentication_classes
=
[
OAuth2AuthAllowInactiveUserDebug
])
),
url
(
r'^oauth2-with-scope-test/$'
,
test_authentication
.
MockView
.
as_view
(
authentication_classes
=
[
OAuth2AuthenticationAllowInactiveUser
],
permission_classes
=
[
permissions
.
TokenHasReadWriteScope
]
)
)
)
)
,
)
)
)
class
OAuth2AuthenticationAllowInactiveUserTestCase
(
test_authentication
.
OAuth2Tests
):
"""
class
OAuth2Tests
(
TestCase
):
Tests the OAuth2AuthenticationAllowInactiveUser class by running all the existing tests in
"""OAuth 2.0 authentication"""
OAuth2Tests but with the is_active flag on the user set to False.
urls
=
'openedx.core.lib.api.tests.test_authentication'
"""
def
setUp
(
self
):
def
setUp
(
self
):
self
.
csrf_client
=
APIClient
(
enforce_csrf_checks
=
True
)
super
(
OAuth2AuthenticationAllowInactiveUserTestCase
,
self
)
.
setUp
()
self
.
username
=
'john'
self
.
email
=
'lennon@thebeatles.com'
self
.
password
=
'password'
self
.
user
=
User
.
objects
.
create_user
(
self
.
username
,
self
.
email
,
self
.
password
)
self
.
CLIENT_ID
=
'client_key'
# pylint: disable=invalid-name
self
.
CLIENT_SECRET
=
'client_secret'
# pylint: disable=invalid-name
self
.
ACCESS_TOKEN
=
"access_token"
# pylint: disable=invalid-name
self
.
REFRESH_TOKEN
=
"refresh_token"
# pylint: disable=invalid-name
self
.
oauth2_client
=
oauth2_provider
.
oauth2
.
models
.
Client
.
objects
.
create
(
client_id
=
self
.
CLIENT_ID
,
client_secret
=
self
.
CLIENT_SECRET
,
redirect_uri
=
''
,
client_type
=
0
,
name
=
'example'
,
user
=
None
,
)
self
.
access_token
=
oauth2_provider
.
oauth2
.
models
.
AccessToken
.
objects
.
create
(
token
=
self
.
ACCESS_TOKEN
,
client
=
self
.
oauth2_client
,
user
=
self
.
user
,
)
self
.
refresh_token
=
oauth2_provider
.
oauth2
.
models
.
RefreshToken
.
objects
.
create
(
user
=
self
.
user
,
access_token
=
self
.
access_token
,
client
=
self
.
oauth2_client
)
# This is the a change we've made from the django-rest-framework-oauth version
# set the user's is_active flag to False.
# of these tests.
self
.
user
.
is_active
=
False
self
.
user
.
is_active
=
False
self
.
user
.
save
()
self
.
user
.
save
()
# This is the a change we've made from the django-rest-framework-oauth version
# of these tests.
# Override the SCOPE_NAME_DICT setting for tests for oauth2-with-scope-test. This is
# Override the SCOPE_NAME_DICT setting for tests for oauth2-with-scope-test. This is
# needed to support READ and WRITE scopes as they currently aren't supported by the
# needed to support READ and WRITE scopes as they currently aren't supported by the
# edx-auth2-provider, and their scope values collide with other scopes defined in the
# edx-auth2-provider, and their scope values collide with other scopes defined in the
# edx-auth2-provider.
# edx-auth2-provider.
scope
.
SCOPE_NAME_DICT
=
{
'read'
:
constants
.
READ
,
'write'
:
constants
.
WRITE
}
scope
.
SCOPE_NAME_DICT
=
{
'read'
:
constants
.
READ
,
'write'
:
constants
.
WRITE
}
def
_create_authorization_header
(
self
,
token
=
None
):
# pylint: disable=missing-docstring
return
"Bearer {0}"
.
format
(
token
or
self
.
access_token
.
token
)
@unittest.skipUnless
(
oauth2_provider
,
'django-oauth2-provider not installed'
)
def
test_get_form_with_wrong_authorization_header_token_type_failing
(
self
):
"""Ensure that a wrong token type lead to the correct HTTP error status code"""
auth
=
"Wrong token-type-obviously"
response
=
self
.
csrf_client
.
get
(
'/oauth2-test/'
,
{},
HTTP_AUTHORIZATION
=
auth
)
self
.
assertEqual
(
response
.
status_code
,
401
)
response
=
self
.
csrf_client
.
get
(
'/oauth2-test/'
,
HTTP_AUTHORIZATION
=
auth
)
self
.
assertEqual
(
response
.
status_code
,
401
)
@unittest.skipUnless
(
oauth2_provider
,
'django-oauth2-provider not installed'
)
def
test_get_form_with_wrong_authorization_header_token_format_failing
(
self
):
"""Ensure that a wrong token format lead to the correct HTTP error status code"""
auth
=
"Bearer wrong token format"
response
=
self
.
csrf_client
.
get
(
'/oauth2-test/'
,
{},
HTTP_AUTHORIZATION
=
auth
)
self
.
assertEqual
(
response
.
status_code
,
401
)
response
=
self
.
csrf_client
.
get
(
'/oauth2-test/'
,
HTTP_AUTHORIZATION
=
auth
)
self
.
assertEqual
(
response
.
status_code
,
401
)
@unittest.skipUnless
(
oauth2_provider
,
'django-oauth2-provider not installed'
)
def
test_get_form_with_wrong_authorization_header_token_failing
(
self
):
"""Ensure that a wrong token lead to the correct HTTP error status code"""
auth
=
"Bearer wrong-token"
response
=
self
.
csrf_client
.
get
(
'/oauth2-test/'
,
{},
HTTP_AUTHORIZATION
=
auth
)
self
.
assertEqual
(
response
.
status_code
,
401
)
response
=
self
.
csrf_client
.
get
(
'/oauth2-test/'
,
HTTP_AUTHORIZATION
=
auth
)
self
.
assertEqual
(
response
.
status_code
,
401
)
@unittest.skipUnless
(
oauth2_provider
,
'django-oauth2-provider not installed'
)
def
test_get_form_with_wrong_authorization_header_token_missing
(
self
):
"""Ensure that a missing token lead to the correct HTTP error status code"""
auth
=
"Bearer"
response
=
self
.
csrf_client
.
get
(
'/oauth2-test/'
,
{},
HTTP_AUTHORIZATION
=
auth
)
self
.
assertEqual
(
response
.
status_code
,
401
)
response
=
self
.
csrf_client
.
get
(
'/oauth2-test/'
,
HTTP_AUTHORIZATION
=
auth
)
self
.
assertEqual
(
response
.
status_code
,
401
)
@unittest.skipUnless
(
oauth2_provider
,
'django-oauth2-provider not installed'
)
def
test_get_form_passing_auth
(
self
):
"""Ensure GETing form over OAuth with correct client credentials succeed"""
auth
=
self
.
_create_authorization_header
()
response
=
self
.
csrf_client
.
get
(
'/oauth2-test/'
,
HTTP_AUTHORIZATION
=
auth
)
self
.
assertEqual
(
response
.
status_code
,
200
)
@unittest.skipUnless
(
oauth2_provider
,
'django-oauth2-provider not installed'
)
def
test_post_form_passing_auth_url_transport
(
self
):
"""Ensure GETing form over OAuth with correct client credentials in form data succeed"""
response
=
self
.
csrf_client
.
post
(
'/oauth2-test/'
,
data
=
{
'access_token'
:
self
.
access_token
.
token
}
)
self
.
assertEqual
(
response
.
status_code
,
200
)
@unittest.skipUnless
(
oauth2_provider
,
'django-oauth2-provider not installed'
)
def
test_get_form_passing_auth_url_transport
(
self
):
"""Ensure GETing form over OAuth with correct client credentials in query succeed when DEBUG is True"""
query
=
urlencode
({
'access_token'
:
self
.
access_token
.
token
})
response
=
self
.
csrf_client
.
get
(
'/oauth2-test-debug/?
%
s'
%
query
)
self
.
assertEqual
(
response
.
status_code
,
200
)
@unittest.skipUnless
(
oauth2_provider
,
'django-oauth2-provider not installed'
)
def
test_get_form_failing_auth_url_transport
(
self
):
"""Ensure GETing form over OAuth with correct client credentials in query fails when DEBUG is False"""
query
=
urlencode
({
'access_token'
:
self
.
access_token
.
token
})
response
=
self
.
csrf_client
.
get
(
'/oauth2-test/?
%
s'
%
query
)
self
.
assertIn
(
response
.
status_code
,
(
status
.
HTTP_401_UNAUTHORIZED
,
status
.
HTTP_403_FORBIDDEN
))
@unittest.skipUnless
(
oauth2_provider
,
'django-oauth2-provider not installed'
)
def
test_post_form_passing_auth
(
self
):
"""Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF"""
auth
=
self
.
_create_authorization_header
()
response
=
self
.
csrf_client
.
post
(
'/oauth2-test/'
,
HTTP_AUTHORIZATION
=
auth
)
self
.
assertEqual
(
response
.
status_code
,
200
)
@unittest.skipUnless
(
oauth2_provider
,
'django-oauth2-provider not installed'
)
def
test_post_form_token_removed_failing_auth
(
self
):
"""Ensure POSTing when there is no OAuth access token in db fails"""
self
.
access_token
.
delete
()
auth
=
self
.
_create_authorization_header
()
response
=
self
.
csrf_client
.
post
(
'/oauth2-test/'
,
HTTP_AUTHORIZATION
=
auth
)
self
.
assertIn
(
response
.
status_code
,
(
status
.
HTTP_401_UNAUTHORIZED
,
status
.
HTTP_403_FORBIDDEN
))
@unittest.skipUnless
(
oauth2_provider
,
'django-oauth2-provider not installed'
)
def
test_post_form_with_refresh_token_failing_auth
(
self
):
"""Ensure POSTing with refresh token instead of access token fails"""
auth
=
self
.
_create_authorization_header
(
token
=
self
.
refresh_token
.
token
)
response
=
self
.
csrf_client
.
post
(
'/oauth2-test/'
,
HTTP_AUTHORIZATION
=
auth
)
self
.
assertIn
(
response
.
status_code
,
(
status
.
HTTP_401_UNAUTHORIZED
,
status
.
HTTP_403_FORBIDDEN
))
@unittest.skipUnless
(
oauth2_provider
,
'django-oauth2-provider not installed'
)
def
test_post_form_with_expired_access_token_failing_auth
(
self
):
"""Ensure POSTing with expired access token fails with an 'Invalid token' error"""
self
.
access_token
.
expires
=
datetime
.
datetime
.
now
()
-
datetime
.
timedelta
(
seconds
=
10
)
# 10 seconds late
self
.
access_token
.
save
()
auth
=
self
.
_create_authorization_header
()
response
=
self
.
csrf_client
.
post
(
'/oauth2-test/'
,
HTTP_AUTHORIZATION
=
auth
)
self
.
assertIn
(
response
.
status_code
,
(
status
.
HTTP_401_UNAUTHORIZED
,
status
.
HTTP_403_FORBIDDEN
))
self
.
assertIn
(
'Invalid token'
,
response
.
content
)
@unittest.skipUnless
(
oauth2_provider
,
'django-oauth2-provider not installed'
)
def
test_post_form_with_invalid_scope_failing_auth
(
self
):
"""Ensure POSTing with a readonly scope instead of a write scope fails"""
read_only_access_token
=
self
.
access_token
read_only_access_token
.
scope
=
oauth2_provider_scope
.
SCOPE_NAME_DICT
[
'read'
]
read_only_access_token
.
save
()
auth
=
self
.
_create_authorization_header
(
token
=
read_only_access_token
.
token
)
response
=
self
.
csrf_client
.
get
(
'/oauth2-with-scope-test/'
,
HTTP_AUTHORIZATION
=
auth
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response
=
self
.
csrf_client
.
post
(
'/oauth2-with-scope-test/'
,
HTTP_AUTHORIZATION
=
auth
)
self
.
assertEqual
(
response
.
status_code
,
status
.
HTTP_403_FORBIDDEN
)
@unittest.skipUnless
(
oauth2_provider
,
'django-oauth2-provider not installed'
)
def
test_post_form_with_valid_scope_passing_auth
(
self
):
"""Ensure POSTing with a write scope succeed"""
read_write_access_token
=
self
.
access_token
read_write_access_token
.
scope
=
oauth2_provider_scope
.
SCOPE_NAME_DICT
[
'write'
]
read_write_access_token
.
save
()
auth
=
self
.
_create_authorization_header
(
token
=
read_write_access_token
.
token
)
response
=
self
.
csrf_client
.
post
(
'/oauth2-with-scope-test/'
,
HTTP_AUTHORIZATION
=
auth
)
self
.
assertEqual
(
response
.
status_code
,
200
)
openedx/core/lib/api/view_utils.py
View file @
2fd6add5
...
@@ -9,7 +9,6 @@ from django.utils.translation import ugettext as _
...
@@ -9,7 +9,6 @@ from django.utils.translation import ugettext as _
from
rest_framework
import
status
,
response
from
rest_framework
import
status
,
response
from
rest_framework.exceptions
import
APIException
from
rest_framework.exceptions
import
APIException
from
rest_framework.permissions
import
IsAuthenticated
from
rest_framework.permissions
import
IsAuthenticated
from
rest_framework.request
import
clone_request
from
rest_framework.response
import
Response
from
rest_framework.response
import
Response
from
rest_framework.mixins
import
RetrieveModelMixin
,
UpdateModelMixin
from
rest_framework.mixins
import
RetrieveModelMixin
,
UpdateModelMixin
from
rest_framework.generics
import
GenericAPIView
from
rest_framework.generics
import
GenericAPIView
...
@@ -194,23 +193,3 @@ class RetrievePatchAPIView(RetrieveModelMixin, UpdateModelMixin, GenericAPIView)
...
@@ -194,23 +193,3 @@ class RetrievePatchAPIView(RetrieveModelMixin, UpdateModelMixin, GenericAPIView)
add_serializer_errors
(
serializer
,
patch
,
field_errors
)
add_serializer_errors
(
serializer
,
patch
,
field_errors
)
return
field_errors
return
field_errors
def
get_object_or_none
(
self
):
"""
Retrieve an object or return None if the object can't be found.
NOTE: This replaces functionality that was removed in Django Rest Framework v3.1.
"""
try
:
return
self
.
get_object
()
except
Http404
:
if
self
.
request
.
method
==
'PUT'
:
# For PUT-as-create operation, we need to ensure that we have
# relevant permissions, as if this was a POST request. This
# will either raise a PermissionDenied exception, or simply
# return None.
self
.
check_permissions
(
clone_request
(
self
.
request
,
'POST'
))
else
:
# PATCH requests where the object does not exist should still
# return a 404 response.
raise
requirements/edx/base.txt
View file @
2fd6add5
...
@@ -28,7 +28,7 @@ django-ses==0.7.0
...
@@ -28,7 +28,7 @@ django-ses==0.7.0
django-simple-history==1.6.3
django-simple-history==1.6.3
django-storages==1.1.5
django-storages==1.1.5
django-method-override==0.1.0
django-method-override==0.1.0
djangorestframework
>=3.1,<3.2
djangorestframework
==2.3.14
django==1.4.22
django==1.4.22
elasticsearch==0.4.5
elasticsearch==0.4.5
facebook-sdk==0.4.0
facebook-sdk==0.4.0
...
...
requirements/edx/github.txt
View file @
2fd6add5
...
@@ -12,7 +12,6 @@ git+https://github.com/edx/django-staticfiles.git@031bdeaea85798b8c284e2a09977df
...
@@ -12,7 +12,6 @@ git+https://github.com/edx/django-staticfiles.git@031bdeaea85798b8c284e2a09977df
-e git+https://github.com/edx/django-pipeline.git@88ec8a011e481918fdc9d2682d4017c835acd8be#egg=django-pipeline
-e git+https://github.com/edx/django-pipeline.git@88ec8a011e481918fdc9d2682d4017c835acd8be#egg=django-pipeline
-e git+https://github.com/edx/django-wiki.git@cd0b2b31997afccde519fe5b3365e61a9edb143f#egg=django-wiki
-e git+https://github.com/edx/django-wiki.git@cd0b2b31997afccde519fe5b3365e61a9edb143f#egg=django-wiki
-e git+https://github.com/edx/django-oauth2-provider.git@0.2.7-fork-edx-5#egg=django-oauth2-provider
-e git+https://github.com/edx/django-oauth2-provider.git@0.2.7-fork-edx-5#egg=django-oauth2-provider
-e git+https://github.com/edx/django-rest-framework-oauth.git@f0b503fda8c254a38f97fef802ded4f5fe367f7a#egg=djangorestframework-oauth
-e git+https://github.com/edx/MongoDBProxy.git@25b99097615bda06bd7cdfe5669ed80dc2a7fed0#egg=mongodb_proxy
-e git+https://github.com/edx/MongoDBProxy.git@25b99097615bda06bd7cdfe5669ed80dc2a7fed0#egg=mongodb_proxy
git+https://github.com/edx/nltk.git@2.0.6#egg=nltk==2.0.6
git+https://github.com/edx/nltk.git@2.0.6#egg=nltk==2.0.6
-e git+https://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
-e git+https://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
...
@@ -41,13 +40,13 @@ git+https://github.com/edx/rfc6266.git@v0.0.5-edx#egg=rfc6266==0.0.5-edx
...
@@ -41,13 +40,13 @@ git+https://github.com/edx/rfc6266.git@v0.0.5-edx#egg=rfc6266==0.0.5-edx
-e git+https://github.com/edx/event-tracking.git@0.2.0#egg=event-tracking
-e git+https://github.com/edx/event-tracking.git@0.2.0#egg=event-tracking
-e git+https://github.com/edx-solutions/django-splash.git@7579d052afcf474ece1239153cffe1c89935bc4f#egg=django-splash
-e git+https://github.com/edx-solutions/django-splash.git@7579d052afcf474ece1239153cffe1c89935bc4f#egg=django-splash
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
-e git+https://github.com/edx/edx-ora2.git@release-2015-0
9-16T15.28
#egg=edx-ora2
-e git+https://github.com/edx/edx-ora2.git@release-2015-0
8-25T16.16
#egg=edx-ora2
-e git+https://github.com/edx/edx-submissions.git@
0.1.0
#egg=edx-submissions
-e git+https://github.com/edx/edx-submissions.git@
9538ee8a971d04dc1cb05e88f6aa0c36b224455c
#egg=edx-submissions
-e git+https://github.com/edx/opaque-keys.git@27dc382ea587483b1e3889a3d19cbd90b9023a06#egg=opaque-keys
-e git+https://github.com/edx/opaque-keys.git@27dc382ea587483b1e3889a3d19cbd90b9023a06#egg=opaque-keys
git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3
git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3
git+https://github.com/edx/i18n-tools.git@v0.1.3#egg=i18n-tools==v0.1.3
git+https://github.com/edx/i18n-tools.git@v0.1.3#egg=i18n-tools==v0.1.3
git+https://github.com/edx/edx-oauth2-provider.git@0.5.7#egg=oauth2-provider==0.5.7
git+https://github.com/edx/edx-oauth2-provider.git@0.5.7#egg=oauth2-provider==0.5.7
-e git+https://github.com/edx/edx-val.git@
0.0.6
#egg=edx-val
-e git+https://github.com/edx/edx-val.git@
v0.0.5
#egg=edx-val
-e git+https://github.com/pmitros/RecommenderXBlock.git@518234bc354edbfc2651b9e534ddb54f96080779#egg=recommender-xblock
-e git+https://github.com/pmitros/RecommenderXBlock.git@518234bc354edbfc2651b9e534ddb54f96080779#egg=recommender-xblock
-e git+https://github.com/edx/edx-search.git@release-2015-09-11a#egg=edx-search
-e git+https://github.com/edx/edx-search.git@release-2015-09-11a#egg=edx-search
-e git+https://github.com/edx/edx-milestones.git@9b44a37edc3d63a23823c21a63cdd53ef47a7aa4#egg=edx-milestones
-e git+https://github.com/edx/edx-milestones.git@9b44a37edc3d63a23823c21a63cdd53ef47a7aa4#egg=edx-milestones
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment