Commit 4bbe9a20 by Nimisha Asthagiri

Merge pull request #11326 from edx/mobile/remove-dead-code

Mobile API: remove unused endpoints
parents 8f7c158f c088df08
...@@ -73,10 +73,6 @@ class CourseMetadata(object): ...@@ -73,10 +73,6 @@ class CourseMetadata(object):
if not settings.FEATURES.get('ENABLE_VIDEO_UPLOAD_PIPELINE'): if not settings.FEATURES.get('ENABLE_VIDEO_UPLOAD_PIPELINE'):
filtered_list.append('video_upload_pipeline') filtered_list.append('video_upload_pipeline')
# Do not show facebook_url if the feature is disabled.
if not settings.FEATURES.get('ENABLE_MOBILE_SOCIAL_FACEBOOK_FEATURES'):
filtered_list.append('facebook_url')
# Do not show social sharing url field if the feature is disabled. # Do not show social sharing url field if the feature is disabled.
if (not hasattr(settings, 'SOCIAL_SHARING_SETTINGS') or if (not hasattr(settings, 'SOCIAL_SHARING_SETTINGS') or
not getattr(settings, 'SOCIAL_SHARING_SETTINGS', {}).get("CUSTOM_COURSE_URLS")): not getattr(settings, 'SOCIAL_SHARING_SETTINGS', {}).get("CUSTOM_COURSE_URLS")):
......
...@@ -338,15 +338,6 @@ class CourseFields(object): ...@@ -338,15 +338,6 @@ class CourseFields(object):
help=_("Enter the unique identifier for your course's video files provided by edX."), help=_("Enter the unique identifier for your course's video files provided by edX."),
scope=Scope.settings scope=Scope.settings
) )
facebook_url = String(
help=_(
"Enter the URL for the official course Facebook group. "
"If you provide a URL, the mobile app includes a button that students can tap to access the group."
),
default=None,
display_name=_("Facebook URL"),
scope=Scope.settings
)
no_grade = Boolean( no_grade = Boolean(
display_name=_("Course Not Graded"), display_name=_("Course Not Graded"),
help=_("Enter true or false. If true, the course will not be graded."), help=_("Enter true or false. If true, the course will not be graded."),
......
...@@ -3,10 +3,8 @@ Run these tests @ Devstack: ...@@ -3,10 +3,8 @@ Run these tests @ Devstack:
paver test_system -s lms --fasttest --verbose --test_id=lms/djangoapps/course_structure_api paver test_system -s lms --fasttest --verbose --test_id=lms/djangoapps/course_structure_api
""" """
# pylint: disable=missing-docstring,invalid-name,maybe-no-member,attribute-defined-outside-init # pylint: disable=missing-docstring,invalid-name,maybe-no-member,attribute-defined-outside-init
from abc import ABCMeta
from datetime import datetime from datetime import datetime
from mock import patch, Mock from mock import patch, Mock
from itertools import product
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -15,13 +13,11 @@ from oauth2_provider.tests.factories import AccessTokenFactory, ClientFactory ...@@ -15,13 +13,11 @@ from oauth2_provider.tests.factories import AccessTokenFactory, ClientFactory
from opaque_keys.edx.locator import CourseLocator from opaque_keys.edx.locator import CourseLocator
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
from xmodule.modulestore.xml import CourseLocationManager from xmodule.modulestore.xml import CourseLocationManager
from xmodule.tests import get_test_system from xmodule.tests import get_test_system
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from courseware.tests.factories import GlobalStaffFactory, StaffFactory from courseware.tests.factories import GlobalStaffFactory, StaffFactory
from openedx.core.djangoapps.content.course_structures.models import CourseStructure from openedx.core.djangoapps.content.course_structures.models import CourseStructure
from openedx.core.djangoapps.content.course_structures.tasks import update_course_structure from openedx.core.djangoapps.content.course_structures.tasks import update_course_structure
...@@ -457,232 +453,3 @@ class CourseGradingPolicyMissingFieldsTests(CourseDetailTestMixin, CourseViewTes ...@@ -457,232 +453,3 @@ class CourseGradingPolicyMissingFieldsTests(CourseDetailTestMixin, CourseViewTes
} }
] ]
self.assertListEqual(response.data, expected) self.assertListEqual(response.data, expected)
#####################################################################################
#
# The following Mixins/Classes collectively test the CourseBlocksAndNavigation view.
#
# The class hierarchy is:
#
# -----------------> CourseBlocksOrNavigationTestMixin <--------------
# | ^ |
# | | |
# | CourseNavigationTestMixin | CourseBlocksTestMixin |
# | ^ ^ | ^ ^ |
# | | | | | | |
# | | | | | | |
# CourseNavigationTests CourseBlocksAndNavigationTests CourseBlocksTests
#
#
# Each Test Mixin is an abstract class that implements tests specific to its
# corresponding functionality.
#
# The concrete Test classes are expected to define the following class fields:
#
# block_navigation_view_type - The view's name as it should be passed to the django
# reverse method.
# container_fields - A list of fields that are expected to be included in the view's
# response for all container block types.
# block_fields - A list of fields that are expected to be included in the view's
# response for all block types.
#
######################################################################################
class CourseBlocksOrNavigationTestMixin(CourseDetailTestMixin, CourseViewTestsMixin):
"""
A Mixin class for testing all views related to Course blocks and/or navigation.
"""
__metaclass__ = ABCMeta
view_supports_debug_mode = False
def setUp(self):
"""
Override the base `setUp` method to enroll the user in the course, since these views
require enrollment for non-staff users.
"""
super(CourseBlocksOrNavigationTestMixin, self).setUp()
CourseEnrollmentFactory(user=self.user, course_id=self.course.id)
def create_user(self):
"""
Override the base `create_user` method to test with non-staff users for these views.
"""
self.user = UserFactory.create()
@property
def view(self):
"""
Returns the name of the view for testing to use in the django `reverse` call.
"""
return 'course_structure_api:v0:' + self.block_navigation_view_type
def test_get(self):
with check_mongo_calls(4):
response = super(CourseBlocksOrNavigationTestMixin, self).test_get()
# verify root element
self.assertIn('root', response.data)
root_string = unicode(self.course.location)
self.assertEquals(response.data['root'], root_string)
# verify ~blocks element
self.assertTrue(self.block_navigation_view_type in response.data)
blocks = response.data[self.block_navigation_view_type]
# verify number of blocks
self.assertEquals(len(blocks), 5)
# verify fields in blocks
for field, block in product(self.block_fields, blocks.values()):
self.assertIn(field, block)
# verify container fields in container blocks
for field in self.container_fields:
self.assertIn(field, blocks[root_string])
def test_parse_error(self):
"""
Verifies the view returns a 400 when a query parameter is incorrectly formatted.
"""
response = self.http_get_for_course(data={'block_json': 'incorrect'})
self.assertEqual(response.status_code, 400)
@SharedModuleStoreTestCase.modifies_courseware
def test_no_access_to_block(self):
"""
Verifies the view returns only the top-level course block, excluding the sequential block
and its descendants when the user does not have access to the sequential.
"""
self.sequential.visible_to_staff_only = True
modulestore().update_item(self.sequential, self.user.id)
response = super(CourseBlocksOrNavigationTestMixin, self).test_get()
self.assertEquals(len(response.data[self.block_navigation_view_type]), 1)
class CourseBlocksTestMixin(object):
"""
A Mixin class for testing all views related to Course blocks.
"""
__metaclass__ = ABCMeta
view_supports_debug_mode = False
block_fields = ['id', 'type', 'display_name', 'web_url', 'block_url', 'graded', 'format']
def test_block_json(self):
"""
Verifies the view's response when the block_json data is requested.
"""
response = self.http_get_for_course(
data={'block_json': '{"video":{"profiles":["mobile_low"]}}'}
)
self.assertEquals(response.status_code, 200)
video_block = response.data[self.block_navigation_view_type][unicode(self.video.location)]
self.assertIn('block_json', video_block)
def test_block_count(self):
"""
Verifies the view's response when the block_count data is requested.
"""
response = self.http_get_for_course(
data={'block_count': 'problem'}
)
self.assertEquals(response.status_code, 200)
root_block = response.data[self.block_navigation_view_type][unicode(self.course.location)]
self.assertIn('block_count', root_block)
self.assertIn('problem', root_block['block_count'])
self.assertEquals(root_block['block_count']['problem'], 1)
def test_multi_device_support(self):
"""
Verifies the view's response when multi_device support is requested.
"""
response = self.http_get_for_course(
data={'fields': 'multi_device'}
)
self.assertEquals(response.status_code, 200)
for block, expected_multi_device_support in (
(self.problem, True),
(self.html, True),
(self.video, False)
):
block_response = response.data[self.block_navigation_view_type][unicode(block.location)]
self.assertEquals(block_response['multi_device'], expected_multi_device_support)
class CourseNavigationTestMixin(object):
"""
A Mixin class for testing all views related to Course navigation.
"""
__metaclass__ = ABCMeta
def test_depth_zero(self):
"""
Tests that all descendants are bundled into the root block when the navigation_depth is set to 0.
"""
response = self.http_get_for_course(
data={'navigation_depth': '0'}
)
root_block = response.data[self.block_navigation_view_type][unicode(self.course.location)]
self.assertIn('descendants', root_block)
self.assertEquals(len(root_block['descendants']), 4)
def test_depth(self):
"""
Tests that all container blocks have descendants listed in their data.
"""
response = self.http_get_for_course()
container_descendants = (
(self.course.location, 1),
(self.sequential.location, 3),
)
for container_location, expected_num_descendants in container_descendants:
block = response.data[self.block_navigation_view_type][unicode(container_location)]
self.assertIn('descendants', block)
self.assertEquals(len(block['descendants']), expected_num_descendants)
class CourseBlocksTests(CourseBlocksOrNavigationTestMixin, CourseBlocksTestMixin, SharedModuleStoreTestCase):
"""
A Test class for testing the Course 'blocks' view.
"""
block_navigation_view_type = 'blocks'
container_fields = ['children']
@classmethod
def setUpClass(cls):
super(CourseBlocksTests, cls).setUpClass()
cls.create_course_data()
class CourseNavigationTests(CourseBlocksOrNavigationTestMixin, CourseNavigationTestMixin, SharedModuleStoreTestCase):
"""
A Test class for testing the Course 'navigation' view.
"""
block_navigation_view_type = 'navigation'
container_fields = ['descendants']
block_fields = []
@classmethod
def setUpClass(cls):
super(CourseNavigationTests, cls).setUpClass()
cls.create_course_data()
class CourseBlocksAndNavigationTests(CourseBlocksOrNavigationTestMixin, CourseBlocksTestMixin,
CourseNavigationTestMixin, SharedModuleStoreTestCase):
"""
A Test class for testing the Course 'blocks+navigation' view.
"""
block_navigation_view_type = 'blocks+navigation'
container_fields = ['children', 'descendants']
@classmethod
def setUpClass(cls):
super(CourseBlocksAndNavigationTests, cls).setUpClass()
cls.create_course_data()
...@@ -20,27 +20,3 @@ urlpatterns = patterns( ...@@ -20,27 +20,3 @@ urlpatterns = patterns(
name='grading_policy' name='grading_policy'
), ),
) )
if settings.FEATURES.get('ENABLE_COURSE_BLOCKS_NAVIGATION_API'):
# TODO (MA-789) This endpoint still needs to be approved by the arch council.
# TODO (MA-704) This endpoint still needs to be made performant.
urlpatterns += (
url(
r'^courses/{}/blocks/$'.format(COURSE_ID_PATTERN),
views.CourseBlocksAndNavigation.as_view(),
{'return_blocks': True, 'return_nav': False},
name='blocks'
),
url(
r'^courses/{}/navigation/$'.format(COURSE_ID_PATTERN),
views.CourseBlocksAndNavigation.as_view(),
{'return_blocks': False, 'return_nav': True},
name='navigation'
),
url(
r'^courses/{}/blocks\+navigation/$'.format(COURSE_ID_PATTERN),
views.CourseBlocksAndNavigation.as_view(),
{'return_blocks': True, 'return_nav': True},
name='blocks+navigation'
),
)
""" API implementation for course-oriented interactions. """ """ API implementation for course-oriented interactions. """
from collections import namedtuple
import json
import logging import logging
from django.conf import settings from django.conf import settings
...@@ -12,20 +10,15 @@ from rest_framework.exceptions import AuthenticationFailed, ParseError ...@@ -12,20 +10,15 @@ 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
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.reverse import reverse
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from course_structure_api.v0 import serializers from course_structure_api.v0 import serializers
from courseware import courses from courseware import courses
from courseware.access import has_access from courseware.access import has_access
from courseware.model_data import FieldDataCache
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.djangoapps.content.course_structures.api.v0 import api, errors from openedx.core.djangoapps.content.course_structures.api.v0 import api, errors
from openedx.core.lib.exceptions import CourseNotFoundError from openedx.core.lib.exceptions import CourseNotFoundError
from student.roles import CourseInstructorRole, CourseStaffRole from student.roles import CourseInstructorRole, CourseStaffRole
from util.module_utils import get_dynamic_descriptor_children
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -297,396 +290,3 @@ class CourseGradingPolicy(CourseViewMixin, ListAPIView): ...@@ -297,396 +290,3 @@ class CourseGradingPolicy(CourseViewMixin, ListAPIView):
@CourseViewMixin.course_check @CourseViewMixin.course_check
def get(self, request, **kwargs): def get(self, request, **kwargs):
return Response(api.course_grading_policy(self.course_key)) return Response(api.course_grading_policy(self.course_key))
@view_auth_classes()
class CourseBlocksAndNavigation(ListAPIView):
"""
**Use Case**
The following endpoints return the content of the course according to the requesting user's access level.
* Blocks - Get the course's blocks.
* Navigation - Get the course's navigation information per the navigation depth requested.
* Blocks+Navigation - Get both the course's blocks and the course's navigation information.
**Example requests**:
GET api/course_structure/v0/courses/{course_id}/blocks/
GET api/course_structure/v0/courses/{course_id}/navigation/
GET api/course_structure/v0/courses/{course_id}/blocks+navigation/
&block_count=video
&block_json={"video":{"profiles":["mobile_low"]}}
&fields=graded,format,multi_device
**Parameters**:
* block_json: (dict) Indicates for which block types to return student_view_json data. The key is the block
type and the value is the "context" that is passed to the block's student_view_json method.
Example: block_json={"video":{"profiles":["mobile_high","mobile_low"]}}
* block_count: (list) Indicates for which block types to return the aggregate count of the blocks.
Example: block_count="video,problem"
* fields: (list) Indicates which additional fields to return for each block.
Default is "children,graded,format,multi_device"
Example: fields=graded,format,multi_device
* navigation_depth (integer) Indicates how far deep to traverse into the course hierarchy before bundling
all the descendants.
Default is 3 since typical navigational views of the course show a maximum of chapter->sequential->vertical.
Example: navigation_depth=3
**Response Values**
The following fields are returned with a successful response.
Only either one of blocks, navigation, or blocks+navigation is returned depending on which endpoint is used.
The "root" field is returned for all endpoints.
* root: The ID of the root node of the course blocks.
* blocks: A dictionary that maps block usage IDs to a collection of information about each block.
Each block contains the following fields. Returned only if using the "blocks" endpoint.
* id: (string) The usage ID of the block.
* type: (string) The type of block. Possible values include course, chapter, sequential, vertical, html,
problem, video, and discussion. The type can also be the name of a custom type of block used for the course.
* display_name: (string) The display name of the block.
* children: (list) If the block has child blocks, a list of IDs of the child blocks.
Returned only if the "children" input parameter is True.
* block_count: (dict) For each block type specified in the block_count parameter to the endpoint, the
aggregate number of blocks of that type for this block and all of its descendants.
Returned only if the "block_count" input parameter contains this block's type.
* block_json: (dict) The JSON data for this block.
Returned only if the "block_json" input parameter contains this block's type.
* block_url: (string) The URL to retrieve the HTML rendering of this block. The HTML could include
CSS and Javascript code. This URL can be used as a fallback if the custom block_json for this
block type is not requested and not supported.
* web_url: (string) The URL to the website location of this block. This URL can be used as a further
fallback if the block_url and the block_json is not supported.
* graded (boolean) Whether or not the block or any of its descendants is graded.
Returned only if "graded" is included in the "fields" parameter.
* format: (string) The assignment type of the block.
Possible values can be "Homework", "Lab", "Midterm Exam", and "Final Exam".
Returned only if "format" is included in the "fields" parameter.
* multi_device: (boolean) Whether or not the block's rendering obtained via block_url has support
for multiple devices.
Returned only if "multi_device" is included in the "fields" parameter.
* navigation: A dictionary that maps block IDs to a collection of navigation information about each block.
Each block contains the following fields. Returned only if using the "navigation" endpoint.
* descendants: (list) A list of IDs of the children of the block if the block's depth in the
course hierarchy is less than the navigation_depth. Otherwise, a list of IDs of the aggregate descendants
of the block.
* blocks+navigation: A dictionary that combines both the blocks and navigation data.
Returned only if using the "blocks+navigation" endpoint.
"""
class RequestInfo(object):
"""
A class for encapsulating the request information, including what optional fields are requested.
"""
DEFAULT_FIELDS = "children,graded,format,multi_device"
def __init__(self, request, course):
self.request = request
self.course = course
self.field_data_cache = None
# check what fields are requested
try:
# fields
self.fields = set(request.GET.get('fields', self.DEFAULT_FIELDS).split(","))
# block_count
self.block_count = request.GET.get('block_count', "")
self.block_count = (
self.block_count.split(",") if self.block_count else []
)
# navigation_depth
# See docstring for why we default to 3.
self.navigation_depth = int(request.GET.get('navigation_depth', '3'))
# block_json
self.block_json = json.loads(request.GET.get('block_json', "{}"))
if self.block_json and not isinstance(self.block_json, dict):
raise ParseError
except:
raise ParseError
class ResultData(object):
"""
A class for encapsulating the result information, specifically the blocks and navigation data.
"""
def __init__(self, return_blocks, return_nav):
self.blocks = {}
self.navigation = {}
if return_blocks and return_nav:
self.navigation = self.blocks
def update_response(self, response, return_blocks, return_nav):
"""
Updates the response object with result information.
"""
if return_blocks and return_nav:
response["blocks+navigation"] = self.blocks
elif return_blocks:
response["blocks"] = self.blocks
elif return_nav:
response["navigation"] = self.navigation
class BlockInfo(object):
"""
A class for encapsulating a block's information as needed during traversal of a block hierarchy.
"""
def __init__(self, block, request_info, parent_block_info=None):
# the block for which the recursion is being computed
self.block = block
# the type of the block
self.type = block.category
# the block's depth in the block hierarchy
self.depth = 0
# the block's children
self.children = []
# descendants_of_parent: the list of descendants for this block's parent
self.descendants_of_parent = []
self.descendants_of_self = []
# if a parent block was provided, update this block's data based on the parent's data
if parent_block_info:
# increment this block's depth value
self.depth = parent_block_info.depth + 1
# set this blocks' descendants_of_parent
self.descendants_of_parent = parent_block_info.descendants_of_self
# add ourselves to the parent's children, if requested.
if 'children' in request_info.fields:
parent_block_info.value.setdefault("children", []).append(unicode(block.location))
# the block's data to include in the response
self.value = {
"id": unicode(block.location),
"type": self.type,
"display_name": block.display_name,
"web_url": reverse(
"jump_to",
kwargs={"course_id": unicode(request_info.course.id), "location": unicode(block.location)},
request=request_info.request,
),
"block_url": reverse(
"courseware.views.render_xblock",
kwargs={"usage_key_string": unicode(block.location)},
request=request_info.request,
),
}
@view_course_access(depth=None)
def list(self, request, course, return_blocks=True, return_nav=True, *args, **kwargs):
"""
REST API endpoint for listing all the blocks and/or navigation information in the course,
while regarding user access and roles.
Arguments:
request - Django request object
course - course module object
return_blocks - If true, returns the blocks information for the course.
return_nav - If true, returns the navigation information for the course.
"""
# set starting point
start_block = course
# initialize request and result objects
request_info = self.RequestInfo(request, course)
result_data = self.ResultData(return_blocks, return_nav)
# create and populate a field data cache by pre-fetching for the course (with depth=None)
request_info.field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course.id, request.user, course, depth=None,
)
# start the recursion with the start_block
self.recurse_blocks_nav(request_info, result_data, self.BlockInfo(start_block, request_info))
# return response
response = {"root": unicode(start_block.location)}
result_data.update_response(response, return_blocks, return_nav)
return Response(response)
def recurse_blocks_nav(self, request_info, result_data, block_info):
"""
A depth-first recursive function that supports calculation of both the list of blocks in the course
and the navigation information up to the requested navigation_depth of the course.
Arguments:
request_info - Object encapsulating the request information.
result_data - Running result data that is updated during the recursion.
block_info - Information about the current block in the recursion.
"""
# bind user data to the block
block_info.block = get_module_for_descriptor(
request_info.request.user,
request_info.request,
block_info.block,
request_info.field_data_cache,
request_info.course.id,
course=request_info.course
)
# verify the user has access to this block
if (block_info.block is None or not has_access(
request_info.request.user,
'load',
block_info.block,
course_key=request_info.course.id
)):
return
# add the block's value to the result
result_data.blocks[unicode(block_info.block.location)] = block_info.value
# descendants
self.update_descendants(request_info, result_data, block_info)
# children: recursively call the function for each of the children, while supporting dynamic children.
if block_info.block.has_children:
block_info.children = get_dynamic_descriptor_children(block_info.block, request_info.request.user.id)
for child in block_info.children:
self.recurse_blocks_nav(
request_info,
result_data,
self.BlockInfo(child, request_info, parent_block_info=block_info)
)
# block count
self.update_block_count(request_info, result_data, block_info)
# block JSON data
self.add_block_json(request_info, block_info)
# multi-device support
if 'multi_device' in request_info.fields:
block_info.value['multi_device'] = block_info.block.has_support(
getattr(block_info.block, 'student_view', None),
'multi_device'
)
# additional fields
self.add_additional_fields(request_info, block_info)
def update_descendants(self, request_info, result_data, block_info):
"""
Updates the descendants data for the current block.
The current block is added to its parent's descendants if it is visible in the navigation
(i.e., the 'hide_from_toc' setting is False).
Additionally, the block's depth is compared with the navigation_depth parameter to determine whether the
descendants of the block should be added to its own descendants (if block.depth <= navigation_depth)
or to the descendants of the block's parents (if block.depth > navigation_depth).
block_info.descendants_of_self is the list of descendants that is passed to this block's children.
It should be either:
descendants_of_parent - if this block's depth is greater than the requested navigation_depth.
a dangling [] - if this block's hide_from_toc is True.
a referenced [] in navigation[block.location]["descendants"] - if this block's depth is within
the requested navigation depth.
"""
# Blocks with the 'hide_from_toc' setting are accessible, just not navigatable from the table-of-contents.
# If the 'hide_from_toc' setting is set on the block, do not add this block to the parent's descendants
# list and let the block's descendants add themselves to a dangling (unreferenced) descendants list.
if not block_info.block.hide_from_toc:
# add this block to the parent's descendants
block_info.descendants_of_parent.append(unicode(block_info.block.location))
# if this block's depth in the hierarchy is greater than the requested navigation depth,
# have the block's descendants add themselves to the parent's descendants.
if block_info.depth > request_info.navigation_depth:
block_info.descendants_of_self = block_info.descendants_of_parent
# otherwise, have the block's descendants add themselves to this block's descendants by
# referencing/attaching descendants_of_self from this block's navigation value.
else:
result_data.navigation.setdefault(
unicode(block_info.block.location), {}
)["descendants"] = block_info.descendants_of_self
def update_block_count(self, request_info, result_data, block_info):
"""
For all the block types that are requested to be counted, include the count of that block type as
aggregated from the block's descendants.
Arguments:
request_info - Object encapsulating the request information.
result_data - Running result data that is updated during the recursion.
block_info - Information about the current block in the recursion.
"""
for b_type in request_info.block_count:
block_info.value.setdefault("block_count", {})[b_type] = (
sum(
result_data.blocks.get(unicode(child.location), {}).get("block_count", {}).get(b_type, 0)
for child in block_info.children
) +
(1 if b_type == block_info.type else 0)
)
def add_block_json(self, request_info, block_info):
"""
If the JSON data for this block's type is requested, and the block supports the 'student_view_json'
method, add the response from the 'student_view_json" method as the data for the block.
"""
if block_info.type in request_info.block_json:
if getattr(block_info.block, 'student_view_data', None):
block_info.value["block_json"] = block_info.block.student_view_data(
context=request_info.block_json[block_info.type]
)
# A mapping of API-exposed field names to xBlock field names and API field defaults.
BlockApiField = namedtuple('BlockApiField', 'block_field_name api_field_default')
FIELD_MAP = {
'graded': BlockApiField(block_field_name='graded', api_field_default=False),
'format': BlockApiField(block_field_name='format', api_field_default=None),
}
def add_additional_fields(self, request_info, block_info):
"""
Add additional field names and values of the block as requested in the request_info.
"""
for field_name in request_info.fields:
if field_name in self.FIELD_MAP:
block_info.value[field_name] = getattr(
block_info.block,
self.FIELD_MAP[field_name].block_field_name,
self.FIELD_MAP[field_name].api_field_default,
)
def perform_authentication(self, request):
"""
Ensures that the user is authenticated (e.g. not an AnonymousUser)
"""
super(CourseBlocksAndNavigation, self).perform_authentication(request)
if request.user.is_anonymous():
raise AuthenticationFailed
"""
Social Facebook API
"""
# TODO
# There are still some performance and scalability issues that should be
# addressed for the various endpoints in this social_facebook djangoapp.
#
# For the Courses and Friends API:
# For both endpoints, we are retrieving the same data from the Facebook server.
# We are then simply organizing and filtering that data differently for each endpoint.
#
# Here are 3 ideas that can be explored further:
#
# Option 1. The app can just call one endpoint that provides a mapping between CourseIDs and Friends,
# and then cache that data once. The reverse map from Friends to CourseIDs can then be created on the app side.
#
# Option 2. The app once again calls just one endpoint (since the same data is computed for both),
# and caches the data once. The difference from #1 is that the server does the computation of the reverse-map and
# sends both maps down to the client. It's a tradeoff between bandwidth and client-side computation. So the payload
# could be something like:
#
# {
# courses: [
# {course_id: "c/ourse/1", friend_indices: [1, 2, 3]},
# {course_id: "c/ourse/2", friend_indices: [3, 4, 5]},
# ..
# ],
# friends: [
# {username: "friend1", facebook_id: "xxx", course_indices: [2, 7, 9]},
# {username: "friend2", facebook_id: "yyy", course_indices: [1, 4, 3]},
# ...
# ]
# }
#
# Option 3. Alternatively, continue to have separate endpoints, but have both endpoints call the same underlying method
# with a built-in cache.
#
# All 3 options can make use of a common cache of results from FB.
#
# At a minimum, some performance/load testing would need to be done
# so we have an idea of these endpoints' limitations and thresholds.
"""
A models.py is required to make this an app (until we move to Django 1.7)
"""
"""
Serializer for courses API
"""
from rest_framework import serializers
class CoursesWithFriendsSerializer(serializers.Serializer):
"""
Serializes oauth token for facebook groups request
"""
oauth_token = serializers.CharField(required=True)
# pylint: disable=E1101, W0201
"""
Tests for Courses
"""
import httpretty
import json
from django.core.urlresolvers import reverse
from xmodule.modulestore.tests.factories import CourseFactory
from opaque_keys.edx.keys import CourseKey
from ..test_utils import SocialFacebookTestCase
class TestCourses(SocialFacebookTestCase):
"""
Tests for /api/mobile/v0.5/courses/...
"""
def setUp(self):
super(TestCourses, self).setUp()
self.course = CourseFactory.create(mobile_available=True)
@httpretty.activate
def test_one_course_with_friends(self):
self.user_create_and_signin(1)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], True)
self.set_facebook_interceptor_for_friends(
{'data': [{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']}]}
)
self.enroll_in_course(self.users[1], self.course)
url = reverse('courses-with-friends')
response = self.client.get(url, {'oauth_token': self._FB_USER_ACCESS_TOKEN})
self.assertEqual(response.status_code, 200)
self.assertEqual(self.course.id, CourseKey.from_string(response.data[0]['course']['id'])) # pylint: disable=E1101
@httpretty.activate
def test_two_courses_with_friends(self):
self.user_create_and_signin(1)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], True)
self.enroll_in_course(self.users[1], self.course)
self.course_2 = CourseFactory.create(mobile_available=True)
self.enroll_in_course(self.users[1], self.course_2)
self.set_facebook_interceptor_for_friends(
{'data': [{'name': self.USERS[2]['USERNAME'], 'id': self.USERS[1]['FB_ID']}]}
)
url = reverse('courses-with-friends')
response = self.client.get(url, {'oauth_token': self._FB_USER_ACCESS_TOKEN})
self.assertEqual(response.status_code, 200)
self.assertEqual(self.course.id, CourseKey.from_string(response.data[0]['course']['id'])) # pylint: disable=E1101
self.assertEqual(self.course_2.id, CourseKey.from_string(response.data[1]['course']['id'])) # pylint: disable=E1101
@httpretty.activate
def test_three_courses_but_only_two_unique(self):
self.user_create_and_signin(1)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], True)
self.course_2 = CourseFactory.create(mobile_available=True)
self.enroll_in_course(self.users[1], self.course_2)
self.enroll_in_course(self.users[1], self.course)
self.user_create_and_signin(2)
self.link_edx_account_to_social(self.users[2], self.BACKEND, self.USERS[2]['FB_ID'])
self.set_sharing_preferences(self.users[2], True)
# Enroll another user in course_2
self.enroll_in_course(self.users[2], self.course_2)
self.set_facebook_interceptor_for_friends(
{'data': [
{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']},
{'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']},
]}
)
url = reverse('courses-with-friends')
response = self.client.get(url, {'oauth_token': self._FB_USER_ACCESS_TOKEN})
self.assertEqual(response.status_code, 200)
self.assertEqual(self.course.id, CourseKey.from_string(response.data[0]['course']['id'])) # pylint: disable=E1101
self.assertEqual(self.course_2.id, CourseKey.from_string(response.data[1]['course']['id'])) # pylint: disable=E1101
# Assert that only two courses are returned
self.assertEqual(len(response.data), 2) # pylint: disable=E1101
@httpretty.activate
def test_two_courses_with_two_friends_on_different_paged_results(self):
self.user_create_and_signin(1)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], True)
self.enroll_in_course(self.users[1], self.course)
self.user_create_and_signin(2)
self.link_edx_account_to_social(self.users[2], self.BACKEND, self.USERS[2]['FB_ID'])
self.set_sharing_preferences(self.users[2], True)
self.course_2 = CourseFactory.create(mobile_available=True)
self.enroll_in_course(self.users[2], self.course_2)
self.set_facebook_interceptor_for_friends(
{
'data': [{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']}],
"paging": {"next": "https://graph.facebook.com/v2.2/me/friends/next"},
"summary": {"total_count": 652}
}
)
# Set the interceptor for the paged
httpretty.register_uri(
httpretty.GET,
"https://graph.facebook.com/v2.2/me/friends/next",
body=json.dumps(
{
"data": [{'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']}],
"paging": {
"previous":
"https://graph.facebook.com/v2.2/10154805434030300/friends?limit=25&offset=25"
},
"summary": {"total_count": 652}
}
),
status=201
)
url = reverse('courses-with-friends')
response = self.client.get(url, {'oauth_token': self._FB_USER_ACCESS_TOKEN})
self.assertEqual(response.status_code, 200)
self.assertEqual(self.course.id, CourseKey.from_string(response.data[0]['course']['id'])) # pylint: disable=E1101
self.assertEqual(self.course_2.id, CourseKey.from_string(response.data[1]['course']['id'])) # pylint: disable=E1101
@httpretty.activate
def test_no_courses_with_friends_because_sharing_pref_off(self):
self.user_create_and_signin(1)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], False)
self.set_facebook_interceptor_for_friends(
{'data': [{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']}]}
)
self.enroll_in_course(self.users[1], self.course)
url = reverse('courses-with-friends')
response = self.client.get(url, {'oauth_token': self._FB_USER_ACCESS_TOKEN})
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 0)
@httpretty.activate
def test_no_courses_with_friends_because_no_auth_token(self):
self.user_create_and_signin(1)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], False)
self.set_facebook_interceptor_for_friends(
{'data': [{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']}]}
)
self.enroll_in_course(self.users[1], self.course)
url = reverse('courses-with-friends')
response = self.client.get(url)
self.assertEqual(response.status_code, 400)
"""
URLs for courses API
"""
from django.conf.urls import patterns, url
from .views import CoursesWithFriends
urlpatterns = patterns(
'mobile_api.social_facebook.courses.views',
url(
r'^friends$',
CoursesWithFriends.as_view(),
name='courses-with-friends'
),
)
"""
Views for courses info API
"""
from rest_framework import generics, status
from rest_framework.response import Response
from courseware.access import is_mobile_available_for_user
from student.models import CourseEnrollment
from lms.djangoapps.mobile_api.social_facebook.courses import serializers
from ...users.serializers import CourseEnrollmentSerializer
from ...utils import mobile_view
from ..utils import get_friends_from_facebook, get_linked_edx_accounts, share_with_facebook_friends
@mobile_view()
class CoursesWithFriends(generics.ListAPIView):
"""
**Use Case**
API endpoint for retrieving all the courses that a user's friends are in.
Note that only friends that allow their courses to be shared will be included.
**Example request**
GET /api/mobile/v0.5/social/facebook/courses/friends
**Response Values**
See UserCourseEnrollmentsList in lms/djangoapps/mobile_api/users for the structure of the response values.
"""
serializer_class = serializers.CoursesWithFriendsSerializer
def list(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.GET)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# Get friends from Facebook
result = get_friends_from_facebook(serializer)
if not isinstance(result, list):
return result
friends_that_are_edx_users = get_linked_edx_accounts(result)
# Filter by sharing preferences
users_with_sharing = [
friend for friend in friends_that_are_edx_users if share_with_facebook_friends(friend)
]
# Get unique enrollments
enrollments = []
for friend in users_with_sharing:
query_set = CourseEnrollment.objects.filter(
user_id=friend['edX_id']
).exclude(course_id__in=[enrollment.course_id for enrollment in enrollments])
enrollments.extend(query_set)
# Get course objects
courses = [
enrollment for enrollment in enrollments if enrollment.course
and is_mobile_available_for_user(self.request.user, enrollment.course)
]
serializer = CourseEnrollmentSerializer(courses, context={'request': request}, many=True)
return Response(serializer.data)
"""
A models.py is required to make this an app (until we move to Django 1.7)
"""
"""
Serializer for Friends API
"""
from rest_framework import serializers
class FriendsInCourseSerializer(serializers.Serializer):
"""
Serializes oauth token for facebook groups request
"""
oauth_token = serializers.CharField(required=True)
# pylint: disable=E1101
"""
Tests for friends
"""
import json
import httpretty
from django.core.urlresolvers import reverse
from xmodule.modulestore.tests.factories import CourseFactory
from ..test_utils import SocialFacebookTestCase
class TestFriends(SocialFacebookTestCase):
"""
Tests for /api/mobile/v0.5/friends/...
"""
def setUp(self):
super(TestFriends, self).setUp()
self.course = CourseFactory.create()
@httpretty.activate
def test_no_friends_enrolled(self):
# User 1 set up
self.user_create_and_signin(1)
# Link user_1's edX account to FB
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], True)
# Set the interceptor
self.set_facebook_interceptor_for_friends(
{
'data':
[
{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']},
{'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']},
{'name': self.USERS[3]['USERNAME'], 'id': self.USERS[3]['FB_ID']},
]
}
)
course_id = unicode(self.course.id)
url = reverse('friends-in-course', kwargs={"course_id": course_id})
response = self.client.get(url, {'format': 'json', 'oauth_token': self._FB_USER_ACCESS_TOKEN})
# Assert that no friends are returned
self.assertEqual(response.status_code, 200)
self.assertTrue('friends' in response.data and len(response.data['friends']) == 0)
@httpretty.activate
def test_no_friends_on_facebook(self):
# User 1 set up
self.user_create_and_signin(1)
# Enroll user_1 in the course
self.enroll_in_course(self.users[1], self.course)
self.set_sharing_preferences(self.users[1], True)
# Link user_1's edX account to FB
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
# Set the interceptor
self.set_facebook_interceptor_for_friends({'data': []})
course_id = unicode(self.course.id)
url = reverse('friends-in-course', kwargs={"course_id": course_id})
response = self.client.get(
url, {'format': 'json', 'oauth_token': self._FB_USER_ACCESS_TOKEN}
)
# Assert that no friends are returned
self.assertEqual(response.status_code, 200)
self.assertTrue('friends' in response.data and len(response.data['friends']) == 0)
@httpretty.activate
def test_no_friends_linked_to_edx(self):
# User 1 set up
self.user_create_and_signin(1)
# Enroll user_1 in the course
self.enroll_in_course(self.users[1], self.course)
self.set_sharing_preferences(self.users[1], True)
# User 2 set up
self.user_create_and_signin(2)
# Enroll user_2 in the course
self.enroll_in_course(self.users[2], self.course)
self.set_sharing_preferences(self.users[2], True)
# User 3 set up
self.user_create_and_signin(3)
# Enroll user_3 in the course
self.enroll_in_course(self.users[3], self.course)
self.set_sharing_preferences(self.users[3], True)
# Set the interceptor
self.set_facebook_interceptor_for_friends(
{
'data':
[
{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']},
{'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']},
{'name': self.USERS[3]['USERNAME'], 'id': self.USERS[3]['FB_ID']},
]
}
)
course_id = unicode(self.course.id)
url = reverse('friends-in-course', kwargs={"course_id": course_id})
response = self.client.get(
url,
{'format': 'json', 'oauth_token': self._FB_USER_ACCESS_TOKEN}
)
# Assert that no friends are returned
self.assertEqual(response.status_code, 200)
self.assertTrue('friends' in response.data and len(response.data['friends']) == 0)
@httpretty.activate
def test_no_friends_share_settings_false(self):
# User 1 set up
self.user_create_and_signin(1)
self.enroll_in_course(self.users[1], self.course)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], False)
self.set_facebook_interceptor_for_friends(
{
'data':
[
{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']},
{'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']},
{'name': self.USERS[3]['USERNAME'], 'id': self.USERS[3]['FB_ID']},
]
}
)
url = reverse('friends-in-course', kwargs={"course_id": unicode(self.course.id)})
response = self.client.get(url, {'format': 'json', 'oauth_token': self._FB_USER_ACCESS_TOKEN})
# Assert that USERNAME_1 is returned
self.assertEqual(response.status_code, 200)
self.assertTrue('friends' in response.data)
self.assertTrue('friends' in response.data and len(response.data['friends']) == 0)
@httpretty.activate
def test_no_friends_no_oauth_token(self):
# User 1 set up
self.user_create_and_signin(1)
self.enroll_in_course(self.users[1], self.course)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], False)
self.set_facebook_interceptor_for_friends(
{
'data':
[
{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']},
{'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']},
{'name': self.USERS[3]['USERNAME'], 'id': self.USERS[3]['FB_ID']},
]
}
)
url = reverse('friends-in-course', kwargs={"course_id": unicode(self.course.id)})
response = self.client.get(url, {'format': 'json'})
# Assert that USERNAME_1 is returned
self.assertEqual(response.status_code, 400)
@httpretty.activate
def test_one_friend_in_course(self):
# User 1 set up
self.user_create_and_signin(1)
self.enroll_in_course(self.users[1], self.course)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], True)
self.set_facebook_interceptor_for_friends(
{
'data':
[
{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']},
{'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']},
{'name': self.USERS[3]['USERNAME'], 'id': self.USERS[3]['FB_ID']},
]
}
)
url = reverse('friends-in-course', kwargs={"course_id": unicode(self.course.id)})
response = self.client.get(url, {'format': 'json', 'oauth_token': self._FB_USER_ACCESS_TOKEN})
# Assert that USERNAME_1 is returned
self.assertEqual(response.status_code, 200)
self.assertTrue('friends' in response.data)
self.assertTrue('id' in response.data['friends'][0])
self.assertTrue(response.data['friends'][0]['id'] == self.USERS[1]['FB_ID'])
self.assertTrue('name' in response.data['friends'][0])
self.assertTrue(response.data['friends'][0]['name'] == self.USERS[1]['USERNAME'])
@httpretty.activate
def test_three_friends_in_course(self):
# User 1 set up
self.user_create_and_signin(1)
self.enroll_in_course(self.users[1], self.course)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], True)
# User 2 set up
self.user_create_and_signin(2)
self.enroll_in_course(self.users[2], self.course)
self.link_edx_account_to_social(self.users[2], self.BACKEND, self.USERS[2]['FB_ID'])
self.set_sharing_preferences(self.users[2], True)
# User 3 set up
self.user_create_and_signin(3)
self.enroll_in_course(self.users[3], self.course)
self.link_edx_account_to_social(self.users[3], self.BACKEND, self.USERS[3]['FB_ID'])
self.set_sharing_preferences(self.users[3], True)
self.set_facebook_interceptor_for_friends(
{
'data':
[
{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']},
{'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']},
{'name': self.USERS[3]['USERNAME'], 'id': self.USERS[3]['FB_ID']},
]
}
)
url = reverse('friends-in-course', kwargs={"course_id": unicode(self.course.id)})
response = self.client.get(
url,
{'format': 'json', 'oauth_token': self._FB_USER_ACCESS_TOKEN}
)
self.assertEqual(response.status_code, 200)
self.assertTrue('friends' in response.data)
# Assert that USERNAME_1 is returned
self.assertTrue(
'id' in response.data['friends'][0] and
response.data['friends'][0]['id'] == self.USERS[1]['FB_ID']
)
self.assertTrue(
'name' in response.data['friends'][0] and
response.data['friends'][0]['name'] == self.USERS[1]['USERNAME']
)
# Assert that USERNAME_2 is returned
self.assertTrue(
'id' in response.data['friends'][1] and
response.data['friends'][1]['id'] == self.USERS[2]['FB_ID']
)
self.assertTrue(
'name' in response.data['friends'][1] and
response.data['friends'][1]['name'] == self.USERS[2]['USERNAME']
)
# Assert that USERNAME_3 is returned
self.assertTrue(
'id' in response.data['friends'][2] and
response.data['friends'][2]['id'] == self.USERS[3]['FB_ID']
)
self.assertTrue(
'name' in response.data['friends'][2] and
response.data['friends'][2]['name'] == self.USERS[3]['USERNAME']
)
@httpretty.activate
def test_three_friends_in_paged_response(self):
# User 1 set up
self.user_create_and_signin(1)
self.enroll_in_course(self.users[1], self.course)
self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
self.set_sharing_preferences(self.users[1], True)
# User 2 set up
self.user_create_and_signin(2)
self.enroll_in_course(self.users[2], self.course)
self.link_edx_account_to_social(self.users[2], self.BACKEND, self.USERS[2]['FB_ID'])
self.set_sharing_preferences(self.users[2], True)
# User 3 set up
self.user_create_and_signin(3)
self.enroll_in_course(self.users[3], self.course)
self.link_edx_account_to_social(self.users[3], self.BACKEND, self.USERS[3]['FB_ID'])
self.set_sharing_preferences(self.users[3], True)
self.set_facebook_interceptor_for_friends(
{
'data': [{'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']}],
"paging": {"next": "https://graph.facebook.com/v2.2/me/friends/next_1"},
"summary": {"total_count": 652}
}
)
# Set the interceptor for the first paged content
httpretty.register_uri(
httpretty.GET,
"https://graph.facebook.com/v2.2/me/friends/next_1",
body=json.dumps(
{
"data": [{'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']}],
"paging": {"next": "https://graph.facebook.com/v2.2/me/friends/next_2"},
"summary": {"total_count": 652}
}
),
status=201
)
# Set the interceptor for the last paged content
httpretty.register_uri(
httpretty.GET,
"https://graph.facebook.com/v2.2/me/friends/next_2",
body=json.dumps(
{
"data": [{'name': self.USERS[3]['USERNAME'], 'id': self.USERS[3]['FB_ID']}],
"paging": {
"previous":
"https://graph.facebook.com/v2.2/10154805434030300/friends?limit=25&offset=25"
},
"summary": {"total_count": 652}
}
),
status=201
)
url = reverse('friends-in-course', kwargs={"course_id": unicode(self.course.id)})
response = self.client.get(url, {'format': 'json', 'oauth_token': self._FB_USER_ACCESS_TOKEN})
self.assertEqual(response.status_code, 200)
self.assertTrue('friends' in response.data)
# Assert that USERNAME_1 is returned
self.assertTrue('id' in response.data['friends'][0])
self.assertTrue(response.data['friends'][0]['id'] == self.USERS[1]['FB_ID'])
self.assertTrue('name' in response.data['friends'][0])
self.assertTrue(response.data['friends'][0]['name'] == self.USERS[1]['USERNAME'])
# Assert that USERNAME_2 is returned
self.assertTrue('id' in response.data['friends'][1])
self.assertTrue(response.data['friends'][1]['id'] == self.USERS[2]['FB_ID'])
self.assertTrue('name' in response.data['friends'][1])
self.assertTrue(response.data['friends'][1]['name'] == self.USERS[2]['USERNAME'])
# Assert that USERNAME_3 is returned
self.assertTrue('id' in response.data['friends'][2])
self.assertTrue(response.data['friends'][2]['id'] == self.USERS[3]['FB_ID'])
self.assertTrue('name' in response.data['friends'][2])
self.assertTrue(response.data['friends'][2]['name'] == self.USERS[3]['USERNAME'])
"""
URLs for friends API
"""
from django.conf.urls import patterns, url
from django.conf import settings
from .views import FriendsInCourse
urlpatterns = patterns(
'mobile_api.social_facebook.friends.views',
url(
r'^course/{}$'.format(settings.COURSE_ID_PATTERN),
FriendsInCourse.as_view(),
name='friends-in-course'
),
)
"""
Views for friends info API
"""
from rest_framework import generics, status
from rest_framework.response import Response
from opaque_keys.edx.keys import CourseKey
from student.models import CourseEnrollment
from ...utils import mobile_view
from ..utils import get_friends_from_facebook, get_linked_edx_accounts, share_with_facebook_friends
from lms.djangoapps.mobile_api.social_facebook.friends import serializers
@mobile_view()
class FriendsInCourse(generics.ListAPIView):
"""
**Use Case**
API endpoint that returns all the users friends that are in the course specified.
Note that only friends that allow their courses to be shared will be included.
**Example request**:
GET /api/mobile/v0.5/social/facebook/friends/course/<course_id>
where course_id is in the form of /edX/DemoX/Demo_Course
**Response Values**
{
"friends": [
{
"name": "test",
"id": "12345",
},
...
]
}
"""
serializer_class = serializers.FriendsInCourseSerializer
def list(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.GET)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# Get all the user's FB friends
result = get_friends_from_facebook(serializer)
if not isinstance(result, list):
return result
def is_member(friend, course_key):
"""
Return true if friend is a member of the course specified by the course_key
"""
return CourseEnrollment.objects.filter(
course_id=course_key,
user_id=friend['edX_id']
).count() == 1
# For each friend check if they are a linked edX user
friends_with_edx_users = get_linked_edx_accounts(result)
# Filter by sharing preferences and enrollment in course
course_key = CourseKey.from_string(kwargs['course_id'])
friends_with_sharing_in_course = [
{'id': friend['id'], 'name': friend['name']}
for friend in friends_with_edx_users
if share_with_facebook_friends(friend) and is_member(friend, course_key)
]
return Response({'friends': friends_with_sharing_in_course})
"""
A models.py is required to make this an app (until we move to Django 1.7)
"""
"""
Serializer for user API
"""
from rest_framework import serializers
from django.core.validators import RegexValidator
class GroupSerializer(serializers.Serializer):
"""
Serializes facebook groups request
"""
name = serializers.CharField(max_length=150)
description = serializers.CharField(max_length=200, required=False)
privacy = serializers.ChoiceField(choices=[("open", "open"), ("closed", "closed")], required=False)
class GroupsMembersSerializer(serializers.Serializer):
"""
Serializes facebook invitations request
"""
member_ids = serializers.CharField(
required=True,
validators=[
RegexValidator(
regex=r'^([\d]+,?)*$',
message='A comma separated list of member ids must be provided',
code='member_ids error'
),
]
)
"""
Tests for groups
"""
import httpretty
from ddt import ddt, data
from django.conf import settings
from django.core.urlresolvers import reverse
from courseware.tests.factories import UserFactory
from ..test_utils import SocialFacebookTestCase
@ddt
class TestGroups(SocialFacebookTestCase):
"""
Tests for /api/mobile/v0.5/social/facebook/groups/...
"""
def setUp(self):
super(TestGroups, self).setUp()
self.user = UserFactory.create()
self.client.login(username=self.user.username, password='test')
# Group Creation and Deletion Tests
@httpretty.activate
def test_create_new_open_group(self):
group_id = '12345678'
status_code = 200
self.set_facebook_interceptor_for_access_token()
self.set_facebook_interceptor_for_groups({'id': group_id}, status_code)
url = reverse('create-delete-group', kwargs={'group_id': ''})
response = self.client.post(
url,
{
'name': 'TheBestGroup',
'description': 'The group for the best people',
'privacy': 'open'
}
)
self.assertEqual(response.status_code, status_code)
self.assertTrue('id' in response.data) # pylint: disable=E1103
self.assertEqual(response.data['id'], group_id) # pylint: disable=E1103
@httpretty.activate
def test_create_new_closed_group(self):
group_id = '12345678'
status_code = 200
self.set_facebook_interceptor_for_access_token()
self.set_facebook_interceptor_for_groups({'id': group_id}, status_code)
# Create new group
url = reverse('create-delete-group', kwargs={'group_id': ''})
response = self.client.post(
url,
{
'name': 'TheBestGroup',
'description': 'The group for the best people',
'privacy': 'closed'
}
)
self.assertEqual(response.status_code, status_code)
self.assertTrue('id' in response.data) # pylint: disable=E1103
self.assertEqual(response.data['id'], group_id) # pylint: disable=E1103
def test_create_new_group_no_name(self):
url = reverse('create-delete-group', kwargs={'group_id': ''})
response = self.client.post(url, {})
self.assertEqual(response.status_code, 400)
def test_create_new_group_with_invalid_name(self):
url = reverse('create-delete-group', kwargs={'group_id': ''})
response = self.client.post(url, {'invalid_name': 'TheBestGroup'})
self.assertEqual(response.status_code, 400)
def test_create_new_group_with_invalid_privacy(self):
url = reverse('create-delete-group', kwargs={'group_id': ''})
response = self.client.post(
url,
{'name': 'TheBestGroup', 'privacy': 'half_open_half_closed'}
)
self.assertEqual(response.status_code, 400)
@httpretty.activate
def test_delete_group_that_exists(self):
# Create new group
group_id = '12345678'
status_code = 200
self.set_facebook_interceptor_for_access_token()
self.set_facebook_interceptor_for_groups({'id': group_id}, status_code)
url = reverse('create-delete-group', kwargs={'group_id': ''})
response = self.client.post(
url,
{
'name': 'TheBestGroup',
'description': 'The group for the best people',
'privacy': 'open'
}
)
self.assertEqual(response.status_code, status_code)
self.assertTrue('id' in response.data) # pylint: disable=E1103
# delete group
httpretty.register_uri(
httpretty.POST,
'https://graph.facebook.com/{}/{}/groups/{}?access_token=FakeToken&method=delete'.format(
settings.FACEBOOK_API_VERSION,
settings.FACEBOOK_APP_ID,
group_id
),
body='{"success": "true"}',
status=status_code
)
response = self.delete_group(response.data['id']) # pylint: disable=E1101
self.assertTrue(response.status_code, status_code)
@httpretty.activate
def test_delete(self):
group_id = '12345678'
status_code = 400
httpretty.register_uri(
httpretty.GET,
'https://graph.facebook.com/oauth/access_token?client_secret={}&grant_type=client_credentials&client_id={}'
.format(
settings.FACEBOOK_APP_SECRET,
settings.FACEBOOK_APP_ID
),
body='FakeToken=FakeToken',
status=200
)
httpretty.register_uri(
httpretty.POST,
'https://graph.facebook.com/{}/{}/groups/{}?access_token=FakeToken&method=delete'.format(
settings.FACEBOOK_API_VERSION,
settings.FACEBOOK_APP_ID,
group_id
),
body='{"error": {"message": "error message"}}',
status=status_code
)
response = self.delete_group(group_id)
self.assertTrue(response.status_code, status_code)
# Member addition and Removal tests
@data('1234,,,,5678,,', 'this00is00not00a00valid00id', '1234,abc,5678', '')
def test_invite_single_member_malformed_member_id(self, member_id):
group_id = '111111111111111'
response = self.invite_to_group(group_id, member_id)
self.assertEqual(response.status_code, 400)
@httpretty.activate
def test_invite_single_member(self):
group_id = '111111111111111'
member_id = '44444444444444444'
status_code = 200
self.set_facebook_interceptor_for_access_token()
self.set_facebook_interceptor_for_members({'success': 'True'}, status_code, group_id, member_id)
response = self.invite_to_group(group_id, member_id)
self.assertEqual(response.status_code, status_code)
self.assertTrue('success' in response.data[member_id])
@httpretty.activate
def test_invite_multiple_members_successfully(self):
member_ids = '222222222222222,333333333333333,44444444444444444'
group_id = '111111111111111'
status_code = 200
self.set_facebook_interceptor_for_access_token()
for member_id in member_ids.split(','):
self.set_facebook_interceptor_for_members({'success': 'True'}, status_code, group_id, member_id)
response = self.invite_to_group(group_id, member_ids)
self.assertEqual(response.status_code, status_code)
for member_id in member_ids.split(','):
self.assertTrue('success' in response.data[member_id])
@httpretty.activate
def test_invite_single_member_unsuccessfully(self):
group_id = '111111111111111'
member_id = '44444444444444444'
status_code = 400
self.set_facebook_interceptor_for_access_token()
self.set_facebook_interceptor_for_members(
{'error': {'message': 'error message'}},
status_code, group_id, member_id
)
response = self.invite_to_group(group_id, member_id)
self.assertEqual(response.status_code, 200)
self.assertTrue('error message' in response.data[member_id])
@httpretty.activate
def test_invite_multiple_members_unsuccessfully(self):
member_ids = '222222222222222,333333333333333,44444444444444444'
group_id = '111111111111111'
status_code = 400
self.set_facebook_interceptor_for_access_token()
for member_id in member_ids.split(','):
self.set_facebook_interceptor_for_members(
{'error': {'message': 'error message'}},
status_code, group_id, member_id
)
response = self.invite_to_group(group_id, member_ids)
self.assertEqual(response.status_code, 200)
for member_id in member_ids.split(','):
self.assertTrue('error message' in response.data[member_id])
"""
URLs for groups API
"""
from django.conf.urls import patterns, url
from .views import Groups, GroupsMembers
urlpatterns = patterns(
'mobile_api.social_facebook.groups.views',
url(
r'^(?P<group_id>[\d]*)$',
Groups.as_view(),
name='create-delete-group'
),
url(
r'^(?P<group_id>[\d]+)/member/(?P<member_id>[\d]*,*)$',
GroupsMembers.as_view(),
name='add-remove-member'
)
)
"""
Views for groups info API
"""
from rest_framework import generics, status, mixins
from rest_framework.response import Response
from django.conf import settings
import facebook
from ...utils import mobile_view
from . import serializers
@mobile_view()
class Groups(generics.CreateAPIView, mixins.DestroyModelMixin):
"""
**Use Case**
An API to Create or Delete course groups.
Note: The Delete is not invoked from the current version of the app
and is used only for testing with facebook dependencies.
**Creation Example request**:
POST /api/mobile/v0.5/social/facebook/groups/
Parameters: name : string,
description : string,
privacy : open/closed
**Creation Response Values**
{"id": group_id}
**Deletion Example request**:
DELETE /api/mobile/v0.5/social/facebook/groups/<group_id>
**Deletion Response Values**
{"success" : "true"}
"""
serializer_class = serializers.GroupSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
try:
app_groups_response = facebook_graph_api().request(
settings.FACEBOOK_API_VERSION + '/' + settings.FACEBOOK_APP_ID + "/groups",
post_args=request.POST.dict()
)
return Response(app_groups_response)
except facebook.GraphAPIError, ex:
return Response({'error': ex.result['error']['message']}, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, *args, **kwargs): # pylint: disable=unused-argument
"""
Deletes the course group.
"""
try:
return Response(
facebook_graph_api().request(
settings.FACEBOOK_API_VERSION + '/' + settings.FACEBOOK_APP_ID + "/groups/" + kwargs['group_id'],
post_args={'method': 'delete'}
)
)
except facebook.GraphAPIError, ex:
return Response({'error': ex.result['error']['message']}, status=status.HTTP_400_BAD_REQUEST)
@mobile_view()
class GroupsMembers(generics.CreateAPIView, mixins.DestroyModelMixin):
"""
**Use Case**
An API to Invite and Remove members to a group
Note: The Remove is not invoked from the current version
of the app and is used only for testing with facebook dependencies.
**Invite Example request**:
POST /api/mobile/v0.5/social/facebook/groups/<group_id>/member/
Parameters: members : int,int,int...
**Invite Response Values**
{"member_id" : success/error_message}
A response with each member_id and whether or not the member was added successfully.
If the member was not added successfully the Facebook error message is provided.
**Remove Example request**:
DELETE /api/mobile/v0.5/social/facebook/groups/<group_id>/member/<member_id>
**Remove Response Values**
{"success" : "true"}
"""
serializer_class = serializers.GroupsMembersSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
graph = facebook_graph_api()
url = settings.FACEBOOK_API_VERSION + '/' + kwargs['group_id'] + "/members"
member_ids = serializer.data['member_ids'].split(',')
response = {}
for member_id in member_ids:
try:
if 'success' in graph.request(url, post_args={'member': member_id}):
response[member_id] = 'success'
except facebook.GraphAPIError, ex:
response[member_id] = ex.result['error']['message']
return Response(response, status=status.HTTP_200_OK)
def delete(self, request, *args, **kwargs): # pylint: disable=unused-argument
"""
Deletes the member from the course group.
"""
try:
return Response(
facebook_graph_api().request(
settings.FACEBOOK_API_VERSION + '/' + kwargs['group_id'] + "/members",
post_args={'method': 'delete', 'member': kwargs['member_id']}
)
)
except facebook.GraphAPIError, ex:
return Response({'error': ex.result['error']['message']}, status=status.HTTP_400_BAD_REQUEST)
def facebook_graph_api():
"""
Returns the result from calling Facebook's Graph API with the app's access token.
"""
return facebook.GraphAPI(facebook.get_app_access_token(settings.FACEBOOK_APP_ID, settings.FACEBOOK_APP_SECRET))
"""
A models.py is required to make this an app (until we move to Django 1.7)
"""
"""
A models.py is required to make this an app (until we move to Django 1.7)
"""
"""
Serializer for Share Settings API
"""
from rest_framework import serializers
class UserSharingSerializar(serializers.Serializer):
"""
Serializes user social settings
"""
share_with_facebook_friends = serializers.BooleanField(required=True)
"""
Tests for users sharing preferences
"""
from django.core.urlresolvers import reverse
from ..test_utils import SocialFacebookTestCase
class StudentProfileViewTest(SocialFacebookTestCase):
""" Tests for the student profile views. """
USERNAME = u'bnotions'
PASSWORD = u'horse'
EMAIL = u'horse@bnotions.com'
FULL_NAME = u'bnotions horse'
def setUp(self):
super(StudentProfileViewTest, self).setUp()
self.user_create_and_signin(1)
def assert_shared_value(self, response, expected_value='True'):
"""
Tests whether the response is successful and whether the
share_with_facebook_friends value is set to the expected value.
"""
self.assertEqual(response.status_code, 200)
self.assertTrue('share_with_facebook_friends' in response.data)
self.assertTrue(expected_value in response.data['share_with_facebook_friends'])
def test_set_preferences_to_true(self):
url = reverse('preferences')
response = self.client.post(url, {'share_with_facebook_friends': 'True'})
self.assert_shared_value(response)
def test_set_preferences_to_false(self):
url = reverse('preferences')
response = self.client.post(url, {'share_with_facebook_friends': 'False'})
self.assert_shared_value(response, 'False')
def test_set_preferences_no_parameters(self):
# Note that if no value is given it will default to False
url = reverse('preferences')
response = self.client.post(url, {})
self.assert_shared_value(response, 'False')
def test_set_preferences_invalid_parameters(self):
# Note that if no value is given it will default to False
# also in the case of invalid parameters
url = reverse('preferences')
response = self.client.post(url, {'bad_param': 'False'})
self.assert_shared_value(response, 'False')
def test_get_preferences_after_setting_them(self):
url = reverse('preferences')
for boolean in ['True', 'False']:
# Set the preference
response = self.client.post(url, {'share_with_facebook_friends': boolean})
self.assert_shared_value(response, boolean)
# Get the preference
response = self.client.get(url)
self.assert_shared_value(response, boolean)
def test_get_preferences_without_setting_them(self):
url = reverse('preferences')
# Get the preference
response = self.client.get(url)
self.assert_shared_value(response, 'False')
"""
URLs for users sharing preferences
"""
from django.conf.urls import patterns, url
from .views import UserSharing
urlpatterns = patterns(
'mobile_api.social_facebook.preferences.views',
url(
r'^preferences/$',
UserSharing.as_view(),
name='preferences'
),
)
"""
Views for users sharing preferences
"""
from rest_framework import generics, status
from rest_framework.response import Response
from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences, set_user_preference
from ...utils import mobile_view
from . import serializers
@mobile_view()
class UserSharing(generics.ListCreateAPIView):
"""
**Use Case**
An API to retrieve or update the users social sharing settings
**GET Example request**:
GET /api/mobile/v0.5/settings/preferences/
**GET Response Values**
{'share_with_facebook_friends': 'True'}
**POST Example request**:
POST /api/mobile/v0.5/settings/preferences/
paramters: share_with_facebook_friends : True
**POST Response Values**
{'share_with_facebook_friends': 'True'}
"""
serializer_class = serializers.UserSharingSerializar
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
value = serializer.data['share_with_facebook_friends']
set_user_preference(request.user, "share_with_facebook_friends", value)
return self.get(request, *args, **kwargs)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def get(self, request, *args, **kwargs):
preferences = get_user_preferences(request.user)
response = {'share_with_facebook_friends': preferences.get('share_with_facebook_friends', 'False')}
return Response(response)
"""
Test utils for Facebook functionality
"""
import httpretty
import json
from rest_framework.test import APITestCase
from django.conf import settings
from django.core.urlresolvers import reverse
from social.apps.django_app.default.models import UserSocialAuth
from course_modes.models import CourseMode
from student.models import CourseEnrollment
from student.views import login_oauth_token
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference, set_user_preference
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from courseware.tests.factories import UserFactory
class SocialFacebookTestCase(ModuleStoreTestCase, APITestCase):
"""
Base Class for social test cases
"""
USERS = {
1: {'USERNAME': "TestUser One",
'EMAIL': "test_one@ebnotions.com",
'PASSWORD': "edx",
'FB_ID': "11111111111111111"},
2: {'USERNAME': "TestUser Two",
'EMAIL': "test_two@ebnotions.com",
'PASSWORD': "edx",
'FB_ID': "22222222222222222"},
3: {'USERNAME': "TestUser Three",
'EMAIL': "test_three@ebnotions.com",
'PASSWORD': "edx",
'FB_ID': "33333333333333333"}
}
BACKEND = "facebook"
USER_URL = "https://graph.facebook.com/me"
UID_FIELD = "id"
_FB_USER_ACCESS_TOKEN = 'ThisIsAFakeFacebookToken'
users = {}
def setUp(self):
super(SocialFacebookTestCase, self).setUp()
def set_facebook_interceptor_for_access_token(self):
"""
Facebook interceptor for groups access_token
"""
httpretty.register_uri(
httpretty.GET,
'https://graph.facebook.com/oauth/access_token?client_secret=' +
settings.FACEBOOK_APP_SECRET + '&grant_type=client_credentials&client_id=' +
settings.FACEBOOK_APP_ID,
body='FakeToken=FakeToken',
status=200
)
def set_facebook_interceptor_for_groups(self, data, status):
"""
Facebook interceptor for groups test
"""
httpretty.register_uri(
httpretty.POST,
'https://graph.facebook.com/' + settings.FACEBOOK_API_VERSION +
'/' + settings.FACEBOOK_APP_ID + '/groups',
body=json.dumps(data),
status=status
)
def set_facebook_interceptor_for_members(self, data, status, group_id, member_id):
"""
Facebook interceptor for group members tests
"""
httpretty.register_uri(
httpretty.POST,
'https://graph.facebook.com/' + settings.FACEBOOK_API_VERSION +
'/' + group_id + '/members?member=' + member_id +
'&access_token=FakeToken',
body=json.dumps(data),
status=status
)
def set_facebook_interceptor_for_friends(self, data):
"""
Facebook interceptor for friends tests
"""
httpretty.register_uri(
httpretty.GET,
"https://graph.facebook.com/v2.2/me/friends",
body=json.dumps(data),
status=201
)
def delete_group(self, group_id):
"""
Invoke the delete groups view
"""
url = reverse('create-delete-group', kwargs={'group_id': group_id})
response = self.client.delete(url)
return response
def invite_to_group(self, group_id, member_ids):
"""
Invoke the invite to group view
"""
url = reverse('add-remove-member', kwargs={'group_id': group_id, 'member_id': ''})
return self.client.post(url, {'member_ids': member_ids})
def remove_from_group(self, group_id, member_id):
"""
Invoke the remove from group view
"""
url = reverse('add-remove-member', kwargs={'group_id': group_id, 'member_id': member_id})
response = self.client.delete(url)
self.assertEqual(response.status_code, 200)
def link_edx_account_to_social(self, user, backend, social_uid):
"""
Register the user to the social auth backend
"""
reverse(login_oauth_token, kwargs={"backend": backend})
UserSocialAuth.objects.create(user=user, provider=backend, uid=social_uid)
def set_sharing_preferences(self, user, boolean_value):
"""
Sets self.user's share settings to boolean_value
"""
# Note that setting the value to boolean will result in the conversion to the unicode form of the boolean.
set_user_preference(user, 'share_with_facebook_friends', boolean_value)
self.assertEqual(get_user_preference(user, 'share_with_facebook_friends'), unicode(boolean_value))
def _change_enrollment(self, action, course_id=None, email_opt_in=None):
"""
Change the student's enrollment status in a course.
Args:
action (string): The action to perform (either "enroll" or "unenroll")
Keyword Args:
course_id (unicode): If provided, use this course ID. Otherwise, use the
course ID created in the setup for this test.
email_opt_in (unicode): If provided, pass this value along as
an additional GET parameter.
"""
if course_id is None:
course_id = unicode(self.course.id)
params = {
'enrollment_action': action,
'course_id': course_id
}
if email_opt_in:
params['email_opt_in'] = email_opt_in
return self.client.post(reverse('change_enrollment'), params)
def user_create_and_signin(self, user_number):
"""
Create a user and sign them in
"""
self.users[user_number] = UserFactory.create(
username=self.USERS[user_number]['USERNAME'],
email=self.USERS[user_number]['EMAIL'],
password=self.USERS[user_number]['PASSWORD']
)
self.client.login(username=self.USERS[user_number]['USERNAME'], password=self.USERS[user_number]['PASSWORD'])
def enroll_in_course(self, user, course):
"""
Enroll a user in the course
"""
resp = self._change_enrollment('enroll', course_id=course.id)
self.assertEqual(resp.status_code, 200)
self.assertTrue(CourseEnrollment.is_enrolled(user, course.id))
course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(user, course.id)
self.assertTrue(is_active)
self.assertEqual(course_mode, CourseMode.DEFAULT_MODE_SLUG)
"""
URLs for Social Facebook
"""
from django.conf.urls import patterns, url, include
urlpatterns = patterns(
'',
url(r'^courses/', include('mobile_api.social_facebook.courses.urls')),
url(r'^friends/', include('mobile_api.social_facebook.friends.urls')),
url(r'^groups/', include('mobile_api.social_facebook.groups.urls')),
)
"""
Common utility methods and decorators for Social Facebook APIs.
"""
import json
import urllib2
import facebook
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import status
from rest_framework.response import Response
from social.apps.django_app.default.models import UserSocialAuth
from openedx.core.djangoapps.user_api.models import UserPreference
from student.models import User
# TODO
# The pagination strategy needs to be further flushed out.
# What is the default page size for the facebook Graph API? 25? Is the page size a parameter that can be tweaked?
# If a user has a large number of friends, we would be calling the FB API num_friends/page_size times.
#
# However, on the app, we don't plan to display all those friends anyway.
# If we do, for scalability, the endpoints themselves would need to be paginated.
def get_pagination(friends):
"""
Get paginated data from FaceBook response
"""
data = friends['data']
while 'paging' in friends and 'next' in friends['paging']:
response = urllib2.urlopen(friends['paging']['next'])
friends = json.loads(response.read())
data = data + friends['data']
return data
def get_friends_from_facebook(serializer):
"""
Return a list with the result of a facebook /me/friends call
using the oauth_token contained within the serializer object.
If facebook returns an error, return a response object containing
the error message.
"""
try:
graph = facebook.GraphAPI(serializer.data['oauth_token'])
friends = graph.request(settings.FACEBOOK_API_VERSION + "/me/friends")
return get_pagination(friends)
except facebook.GraphAPIError, ex:
return Response({'error': ex.result['error']['message']}, status=status.HTTP_400_BAD_REQUEST)
def get_linked_edx_accounts(data):
"""
Return a list of friends from the input that are edx users with the
additional attributes of edX_id and edX_username
"""
friends_that_are_edx_users = []
for friend in data:
query_set = UserSocialAuth.objects.filter(uid=unicode(friend['id']))
if query_set.count() == 1:
friend['edX_id'] = query_set[0].user_id
friend['edX_username'] = query_set[0].user.username
friends_that_are_edx_users.append(friend)
return friends_that_are_edx_users
def share_with_facebook_friends(friend):
"""
Return true if the user's share_with_facebook_friends preference is set to true.
"""
# Calling UserPreference directly because the requesting user may be different (and not is_staff).
try:
existing_user = User.objects.get(username=friend['edX_username'])
except ObjectDoesNotExist:
return False
return UserPreference.get_value(existing_user, 'share_with_facebook_friends') == 'True'
""" """
URLs for mobile API URLs for mobile API
""" """
from django.conf import settings
from django.conf.urls import patterns, url, include from django.conf.urls import patterns, url, include
from .users.views import my_user_info from .users.views import my_user_info
...@@ -13,9 +12,3 @@ urlpatterns = patterns( ...@@ -13,9 +12,3 @@ urlpatterns = patterns(
url(r'^video_outlines/', include('mobile_api.video_outlines.urls')), url(r'^video_outlines/', include('mobile_api.video_outlines.urls')),
url(r'^course_info/', include('mobile_api.course_info.urls')), url(r'^course_info/', include('mobile_api.course_info.urls')),
) )
if settings.FEATURES["ENABLE_MOBILE_SOCIAL_FACEBOOK_FEATURES"]:
urlpatterns += (
url(r'^social/facebook/', include('mobile_api.social_facebook.urls')),
url(r'^settings/', include('mobile_api.social_facebook.preferences.urls')),
)
...@@ -76,14 +76,6 @@ class CourseOverviewField(serializers.RelatedField): ...@@ -76,14 +76,6 @@ class CourseOverviewField(serializers.RelatedField):
kwargs={'course_id': course_id}, kwargs={'course_id': course_id},
request=request, request=request,
), ),
# Note: The following 2 should be deprecated.
'social_urls': {
'facebook': course_overview.facebook_url,
},
'latest_updates': {
'video': None
},
} }
......
...@@ -253,23 +253,6 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest ...@@ -253,23 +253,6 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
) )
) )
def test_no_facebook_url(self):
self.login_and_enroll()
response = self.api_response()
course_data = response.data[0]['course']
self.assertIsNone(course_data['social_urls']['facebook'])
def test_facebook_url(self):
self.login_and_enroll()
self.course.facebook_url = "http://facebook.com/test_group_page"
self.store.update_item(self.course, self.user.id)
response = self.api_response()
course_data = response.data[0]['course']
self.assertEquals(course_data['social_urls']['facebook'], self.course.facebook_url)
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def test_discussion_url(self): def test_discussion_url(self):
self.login_and_enroll() self.login_and_enroll()
......
...@@ -236,7 +236,6 @@ class UserCourseEnrollmentsList(generics.ListAPIView): ...@@ -236,7 +236,6 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
it is enabled, otherwise null. it is enabled, otherwise null.
* end: The end date of the course. * end: The end date of the course.
* id: The unique ID of the course. * id: The unique ID of the course.
* latest_updates: Reserved for future use.
* name: The name of the course. * name: The name of the course.
* number: The course number. * number: The course number.
* org: The organization that created the course. * org: The organization that created the course.
......
...@@ -271,10 +271,6 @@ FEATURES = { ...@@ -271,10 +271,6 @@ FEATURES = {
# Expose Mobile REST API. Note that if you use this, you must also set # Expose Mobile REST API. Note that if you use this, you must also set
# ENABLE_OAUTH2_PROVIDER to True # ENABLE_OAUTH2_PROVIDER to True
'ENABLE_MOBILE_REST_API': False, 'ENABLE_MOBILE_REST_API': False,
'ENABLE_MOBILE_SOCIAL_FACEBOOK_FEATURES': False,
# Enable temporary APIs required for xBlocks on Mobile
'ENABLE_COURSE_BLOCKS_NAVIGATION_API': False,
# Enable the combined login/registration form # Enable the combined login/registration form
'ENABLE_COMBINED_LOGIN_REGISTRATION': False, 'ENABLE_COMBINED_LOGIN_REGISTRATION': False,
......
...@@ -293,9 +293,7 @@ OIDC_COURSE_HANDLER_CACHE_TIMEOUT = 0 ...@@ -293,9 +293,7 @@ OIDC_COURSE_HANDLER_CACHE_TIMEOUT = 0
########################### External REST APIs ################################# ########################### External REST APIs #################################
FEATURES['ENABLE_MOBILE_REST_API'] = True FEATURES['ENABLE_MOBILE_REST_API'] = True
FEATURES['ENABLE_MOBILE_SOCIAL_FACEBOOK_FEATURES'] = True
FEATURES['ENABLE_VIDEO_ABSTRACTION_LAYER_API'] = True FEATURES['ENABLE_VIDEO_ABSTRACTION_LAYER_API'] = True
FEATURES['ENABLE_COURSE_BLOCKS_NAVIGATION_API'] = True
###################### Payment ##############################3 ###################### Payment ##############################3
# Enable fake payment processing page # Enable fake payment processing page
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course_overviews', '0007_courseoverviewimageconfig'),
]
operations = [
migrations.RemoveField(
model_name='courseoverview',
name='facebook_url',
),
]
...@@ -66,7 +66,6 @@ class CourseOverview(TimeStampedModel): ...@@ -66,7 +66,6 @@ class CourseOverview(TimeStampedModel):
# URLs # URLs
course_image_url = TextField() course_image_url = TextField()
facebook_url = TextField(null=True)
social_sharing_url = TextField(null=True) social_sharing_url = TextField(null=True)
end_of_course_survey_url = TextField(null=True) end_of_course_survey_url = TextField(null=True)
...@@ -156,7 +155,6 @@ class CourseOverview(TimeStampedModel): ...@@ -156,7 +155,6 @@ class CourseOverview(TimeStampedModel):
announcement=course.announcement, announcement=course.announcement,
course_image_url=course_image_url(course), course_image_url=course_image_url(course),
facebook_url=course.facebook_url,
social_sharing_url=course.social_sharing_url, social_sharing_url=course.social_sharing_url,
certificates_display_behavior=course.certificates_display_behavior, certificates_display_behavior=course.certificates_display_behavior,
......
...@@ -91,7 +91,6 @@ class CourseOverviewTestCase(ModuleStoreTestCase): ...@@ -91,7 +91,6 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
'display_number_with_default', 'display_number_with_default',
'display_org_with_default', 'display_org_with_default',
'advertised_start', 'advertised_start',
'facebook_url',
'social_sharing_url', 'social_sharing_url',
'certificates_display_behavior', 'certificates_display_behavior',
'certificates_show_before_end', 'certificates_show_before_end',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment