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):
if not settings.FEATURES.get('ENABLE_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.
if (not hasattr(settings, 'SOCIAL_SHARING_SETTINGS') or
not getattr(settings, 'SOCIAL_SHARING_SETTINGS', {}).get("CUSTOM_COURSE_URLS")):
......
......@@ -338,15 +338,6 @@ class CourseFields(object):
help=_("Enter the unique identifier for your course's video files provided by edX."),
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(
display_name=_("Course Not Graded"),
help=_("Enter true or false. If true, the course will not be graded."),
......
......@@ -3,10 +3,8 @@ Run these tests @ Devstack:
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
from abc import ABCMeta
from datetime import datetime
from mock import patch, Mock
from itertools import product
from django.core.urlresolvers import reverse
......@@ -15,13 +13,11 @@ from oauth2_provider.tests.factories import AccessTokenFactory, ClientFactory
from opaque_keys.edx.locator import CourseLocator
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
from xmodule.modulestore.xml import CourseLocationManager
from xmodule.tests import get_test_system
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from courseware.tests.factories import GlobalStaffFactory, StaffFactory
from openedx.core.djangoapps.content.course_structures.models import CourseStructure
from openedx.core.djangoapps.content.course_structures.tasks import update_course_structure
......@@ -457,232 +453,3 @@ class CourseGradingPolicyMissingFieldsTests(CourseDetailTestMixin, CourseViewTes
}
]
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(
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. """
from collections import namedtuple
import json
import logging
from django.conf import settings
......@@ -12,20 +10,15 @@ from rest_framework.exceptions import AuthenticationFailed, ParseError
from rest_framework.generics import RetrieveAPIView, ListAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.reverse import reverse
from xmodule.modulestore.django import modulestore
from opaque_keys.edx.keys import CourseKey
from course_structure_api.v0 import serializers
from courseware import courses
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.lib.exceptions import CourseNotFoundError
from student.roles import CourseInstructorRole, CourseStaffRole
from util.module_utils import get_dynamic_descriptor_children
log = logging.getLogger(__name__)
......@@ -297,396 +290,3 @@ class CourseGradingPolicy(CourseViewMixin, ListAPIView):
@CourseViewMixin.course_check
def get(self, request, **kwargs):
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
"""
from django.conf import settings
from django.conf.urls import patterns, url, include
from .users.views import my_user_info
......@@ -13,9 +12,3 @@ urlpatterns = patterns(
url(r'^video_outlines/', include('mobile_api.video_outlines.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):
kwargs={'course_id': course_id},
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
)
)
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})
def test_discussion_url(self):
self.login_and_enroll()
......
......@@ -236,7 +236,6 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
it is enabled, otherwise null.
* end: The end date of the course.
* id: The unique ID of the course.
* latest_updates: Reserved for future use.
* name: The name of the course.
* number: The course number.
* org: The organization that created the course.
......
......@@ -271,10 +271,6 @@ FEATURES = {
# Expose Mobile REST API. Note that if you use this, you must also set
# ENABLE_OAUTH2_PROVIDER to True
'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_COMBINED_LOGIN_REGISTRATION': False,
......
......@@ -293,9 +293,7 @@ OIDC_COURSE_HANDLER_CACHE_TIMEOUT = 0
########################### External REST APIs #################################
FEATURES['ENABLE_MOBILE_REST_API'] = True
FEATURES['ENABLE_MOBILE_SOCIAL_FACEBOOK_FEATURES'] = True
FEATURES['ENABLE_VIDEO_ABSTRACTION_LAYER_API'] = True
FEATURES['ENABLE_COURSE_BLOCKS_NAVIGATION_API'] = True
###################### Payment ##############################3
# 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):
# URLs
course_image_url = TextField()
facebook_url = TextField(null=True)
social_sharing_url = TextField(null=True)
end_of_course_survey_url = TextField(null=True)
......@@ -156,7 +155,6 @@ class CourseOverview(TimeStampedModel):
announcement=course.announcement,
course_image_url=course_image_url(course),
facebook_url=course.facebook_url,
social_sharing_url=course.social_sharing_url,
certificates_display_behavior=course.certificates_display_behavior,
......
......@@ -91,7 +91,6 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
'display_number_with_default',
'display_org_with_default',
'advertised_start',
'facebook_url',
'social_sharing_url',
'certificates_display_behavior',
'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