Merge pull request #11326 from edx/mobile/remove-dead-code
Mobile API: remove unused endpoints
This commit is contained in:
@@ -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'
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
""" 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
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
"""
|
||||
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.
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Courses API
|
||||
"""
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
A models.py is required to make this an app (until we move to Django 1.7)
|
||||
"""
|
||||
@@ -1,11 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,147 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,15 +0,0 @@
|
||||
"""
|
||||
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'
|
||||
),
|
||||
)
|
||||
@@ -1,65 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Friends API
|
||||
"""
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
A models.py is required to make this an app (until we move to Django 1.7)
|
||||
"""
|
||||
@@ -1,11 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,318 +0,0 @@
|
||||
# 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'])
|
||||
@@ -1,16 +0,0 @@
|
||||
"""
|
||||
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'
|
||||
),
|
||||
)
|
||||
@@ -1,71 +0,0 @@
|
||||
"""
|
||||
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})
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Groups API
|
||||
"""
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
A models.py is required to make this an app (until we move to Django 1.7)
|
||||
"""
|
||||
@@ -1,30 +0,0 @@
|
||||
"""
|
||||
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'
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -1,199 +0,0 @@
|
||||
"""
|
||||
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])
|
||||
@@ -1,20 +0,0 @@
|
||||
"""
|
||||
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'
|
||||
)
|
||||
)
|
||||
@@ -1,143 +0,0 @@
|
||||
"""
|
||||
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))
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
A models.py is required to make this an app (until we move to Django 1.7)
|
||||
"""
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Users Sharing preferences API
|
||||
"""
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
A models.py is required to make this an app (until we move to Django 1.7)
|
||||
"""
|
||||
@@ -1,11 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,67 +0,0 @@
|
||||
"""
|
||||
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')
|
||||
@@ -1,14 +0,0 @@
|
||||
"""
|
||||
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'
|
||||
),
|
||||
)
|
||||
@@ -1,52 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,186 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,11 +0,0 @@
|
||||
"""
|
||||
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')),
|
||||
)
|
||||
@@ -1,76 +0,0 @@
|
||||
"""
|
||||
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'
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# -*- 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',
|
||||
|
||||
Reference in New Issue
Block a user